New IC: Detect changes to class annotations

Both the new and old incremental compilation (IC) analysis rely on
Kotlin class metadata to detect a change.

However, Kotlin metadata currently doesn't contain info about
annotations (KT-57919), so the IC will not be able to detect a change
to them.

With this commit, we'll fix the new IC such that it can detect a change
to class annotations by not relying only on metadata.

We currently scope this fix to the new IC (cross-module analysis) first.
We'll fix this issue for within-module analysis later.

Performance: There seems to be no performance impact from this change.
Snapshotting the 400MB ideaIC-2022.1.4/app.jar takes 4.1s before and
after this change.

Test: Added ClasspathChangesComputerTest.testChangedAnnotations
^KT-58289: Fixed
This commit is contained in:
Hung Nguyen
2023-04-26 14:47:57 +01:00
committed by Space Team
parent 993925f656
commit 0b09be73c6
22 changed files with 316 additions and 231 deletions
@@ -441,7 +441,7 @@ open class IncrementalJvmCache(
storageFile: File,
icContext: IncrementalCompilationContext,
) :
BasicStringMap<Map<String, Any>>(storageFile, MapExternalizer(StringExternalizer, ConstantValueExternalizer), icContext) {
BasicStringMap<Map<String, Long>>(storageFile, MapExternalizer(StringExternalizer, LongExternalizer), icContext) {
operator fun contains(className: JvmClassName): Boolean =
className.internalName in storage
@@ -451,7 +451,7 @@ open class IncrementalJvmCache(
val key = kotlinClassInfo.className.internalName
val oldMap = storage[key] ?: emptyMap()
val newMap = kotlinClassInfo.constantsMap
val newMap = kotlinClassInfo.extraInfo.constantSnapshots
if (newMap.isNotEmpty()) {
storage[key] = newMap
} else {
@@ -489,8 +489,8 @@ open class IncrementalJvmCache(
storage.remove(className.internalName)
}
override fun dumpValue(value: Map<String, Any>): String =
value.dumpMap(Any::toString)
override fun dumpValue(value: Map<String, Long>): String =
value.dumpMap(Long::toString)
}
private inner class PackagePartMap(
@@ -592,7 +592,7 @@ open class IncrementalJvmCache(
val key = kotlinClassInfo.className.internalName
val oldMap = storage[key] ?: emptyMap()
val newMap = kotlinClassInfo.inlineFunctionsAndAccessorsMap
val newMap = kotlinClassInfo.extraInfo.inlineFunctionOrAccessorSnapshots
if (newMap.isNotEmpty()) {
storage[key] = newMap
} else {
@@ -5,8 +5,12 @@
package org.jetbrains.kotlin.incremental
import com.intellij.util.io.DataExternalizer
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClassExcludingMembers
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotField
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotMethod
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.sortClassMembers
import org.jetbrains.kotlin.incremental.KotlinClassInfo.ExtraInfo
import org.jetbrains.kotlin.incremental.storage.ProtoMapValue
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
@@ -16,6 +20,9 @@ import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.FieldNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
/**
* Minimal information about a Kotlin class to compute recompilation-triggering changes during an incremental run of the `KotlinCompile`
@@ -25,16 +32,34 @@ import org.jetbrains.org.objectweb.asm.*
* `KotlinCompile` task and the task needs to support compile avoidance. For example, this class should contain public method signatures,
* and should not contain private method signatures, or method implementations.
*/
class KotlinClassInfo constructor(
class KotlinClassInfo(
val classId: ClassId,
val classKind: KotlinClassHeader.Kind,
val classHeaderData: Array<String>, // Can be empty
val classHeaderStrings: Array<String>, // Can be empty
val multifileClassName: String?, // Not null iff classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART
val constantsMap: Map<String, Any>,
val inlineFunctionsAndAccessorsMap: Map<InlineFunctionOrAccessor, Long>
val extraInfo: ExtraInfo
) {
/** Extra information about a Kotlin class that is not captured in the Kotlin class metadata. */
class ExtraInfo(
/**
* Snapshot of the class excluding its fields and methods and Kotlin metadata (iff classKind == [KotlinClassHeader.Kind.CLASS]).
*
* For example, the class's annotations which are currently not captured by Kotlin metadata (KT-57919) will be captured here.
*
* Note: It also excludes Kotlin metadata as [ExtraInfo] should only contain info not yet captured in Kotlin metadata.
*/
val classSnapshotExcludingMembers: Long?,
/** Snapshots of the class's constants (including their values). The map's keys are the constants' names. */
val constantSnapshots: Map<String, Long>,
/** Snapshots of the class's inline functions and property accessors (including their implementation). */
val inlineFunctionOrAccessorSnapshots: Map<InlineFunctionOrAccessor, Long>
)
val className: JvmClassName by lazy { JvmClassName.byClassId(classId) }
val protoMapValue: ProtoMapValue by lazy {
@@ -84,123 +109,148 @@ class KotlinClassInfo constructor(
}
fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
val (constants, inlineFunctionsAndAccessors) = getConstantsAndInlineFunctionsOrAccessors(classHeader, classContents)
return KotlinClassInfo(
classId,
classHeader.kind,
classHeader.data ?: classHeader.incompatibleData ?: emptyArray(),
classHeader.strings ?: emptyArray(),
classHeader.multifileClassName,
constants.mapKeys { it.key.name },
inlineFunctionsAndAccessors
extraInfo = getExtraInfo(classHeader, classContents)
)
}
}
}
/**
* Parses the class file only once to get both constants and inline functions/property accessors. This is faster than getting them
* separately in two passes.
*/
private fun getConstantsAndInlineFunctionsOrAccessors(
classHeader: KotlinClassHeader,
classContents: ByteArray
): Pair<Map<JvmMemberSignature.Field, Any>, Map<InlineFunctionOrAccessor, Long>> {
val constantsClassVisitor = ConstantsClassVisitor()
val inlineFunctionsAndAccessors = inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true)
private fun getExtraInfo(classHeader: KotlinClassHeader, classContents: ByteArray): ExtraInfo {
// Get the list of (non-private) inline functions and accessors from Kotlin class metadata, then find and snapshot them in the bytecode.
// Note:
// - Some of them may not be found in the bytecode. Specifically, internal/private inline functions/accessors may be removed from the
// bytecode if code shrinker is used. For example, `kotlin-reflect-1.7.20.jar` contains `/kotlin/reflect/jvm/internal/UtilKt.class` in
// which the internal inline function `reflectionCall` appears in the Kotlin class metadata (also in the source file), but not in the
// bytecode. When that happens, we will ignore those methods. It is safe to ignore because the methods are not declared in the bytecode
// and therefore can't be referenced.
// - Look for private methods as well because a *public* inline function/accessor may have a *private* corresponding method in the
// bytecode (see `InlineOnlyKt.isInlineOnlyPrivateInBytecode`).
val inlineFunctionsAndAccessors: List<InlineFunctionOrAccessor> = inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true)
val inlineFunctionOrAccessorSignatures: Map<JvmMemberSignature.Method, InlineFunctionOrAccessor> =
inlineFunctionsAndAccessors.associateBy { it.jvmMethodSignature }
return if (inlineFunctionsAndAccessors.isEmpty()) {
// Handle this case differently to improve performance
// parsingOptions = (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info are not important for constants
ClassReader(classContents).accept(constantsClassVisitor, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG)
Pair(constantsClassVisitor.getResult(), emptyMap())
val parsingOptions = if (inlineFunctionsAndAccessors.isNotEmpty()) {
// Do not pass (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info (e.g., line numbers) are important for inline
// functions/accessors
0
} else {
val inlineFunctionsAndAccessorsClassVisitor = InlineFunctionsAndAccessorsClassVisitor(
inlineFunctionsAndAccessors.map { it.jvmMethodSignature }.toSet(),
constantsClassVisitor
)
// parsingOptions must not include (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info (e.g., line numbers) are important for
// Pass (SKIP_CODE, SKIP_DEBUG) to improve performance as method bodies and debug info are not important when we're not analyzing
// inline functions/accessors
ClassReader(classContents).accept(inlineFunctionsAndAccessorsClassVisitor, 0)
val constantsMap = constantsClassVisitor.getResult()
val methodHashesMap = inlineFunctionsAndAccessorsClassVisitor.getResult()
val inlineFunctionsAndAccessorsMap = inlineFunctionsAndAccessors.mapNotNull { inline ->
// Note that internal/private inline functions may be removed from the bytecode if code shrinker is used. For example,
// `kotlin-reflect-1.7.20.jar` contains `/kotlin/reflect/jvm/internal/UtilKt.class` in which the internal inline function
// `reflectionCall` appears in the Kotlin metadata (also in the source file), but not in the bytecode.
// When that happens (i.e., when the map lookup below returns null), we will ignore the method. It is safe to ignore because the
// method is not declared in the bytecode and therefore can't be referenced.
methodHashesMap[inline.jvmMethodSignature]?.let { inline to it }
}.toMap()
Pair(constantsMap, inlineFunctionsAndAccessorsMap)
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG
}
}
private class ConstantsClassVisitor : ClassVisitor(Opcodes.API_VERSION) {
private val result = mutableMapOf<JvmMemberSignature.Field, Any>()
// Load class contents into a `ClassNode`.
// Note that we'll only load methods that are inline functions/accessors (including private methods -- see comment at the top of
// `getExtraInfo`) as we don't need to snapshot the other methods when computing `ExtraInfo`.
val classNode = ClassNode()
val classReader = ClassReader(classContents)
classReader.accept(SkipMethodClassVisitor(classNode) { it !in inlineFunctionOrAccessorSignatures.keys }, parsingOptions)
sortClassMembers(classNode)
override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? {
if (access and Opcodes.ACC_PRIVATE == Opcodes.ACC_PRIVATE) return null
// Snapshot the class excluding its fields and methods and metadata
val classSnapshotExcludingMembers = if (classHeader.kind == KotlinClassHeader.Kind.CLASS) {
// Also exclude Kotlin metadata (see `ExtraInfo.classSnapshotExcludingMembers`'s kdoc)
snapshotClassExcludingMembers(classNode, alsoExcludeKotlinMetaData = true)
} else null
val staticFinal = Opcodes.ACC_STATIC or Opcodes.ACC_FINAL
if (value != null && access and staticFinal == staticFinal) {
result[JvmMemberSignature.Field(name, desc)] = value
// Snapshot constants
fun FieldNode.isPrivate() = (access and Opcodes.ACC_PRIVATE) != 0
fun FieldNode.isStaticFinal() = (access and (Opcodes.ACC_STATIC or Opcodes.ACC_FINAL)) == (Opcodes.ACC_STATIC or Opcodes.ACC_FINAL)
val constantSnapshots: Map<String, Long> = classNode.fields
.filter { !it.isPrivate() && it.isStaticFinal() }
.associate { it.name to snapshotField(it) }
// Snapshot inline functions and accessors
fun MethodNode.signature() = JvmMemberSignature.Method(name = name, desc = desc)
val inlineFunctionOrAccessorSnapshots: Map<InlineFunctionOrAccessor, Long> = classNode.methods
.associate { methodNode ->
// `methodNode` must be an inline function/accessor because we loaded only inline functions/accessors into `classNode`
inlineFunctionOrAccessorSignatures[methodNode.signature()]!! to snapshotMethod(methodNode, classNode.version)
}
return null
}
fun getResult() = result
return ExtraInfo(classSnapshotExcludingMembers, constantSnapshots, inlineFunctionOrAccessorSnapshots)
}
private class InlineFunctionsAndAccessorsClassVisitor(
private val inlineFunctionsAndAccessors: Set<JvmMemberSignature.Method>,
cv: ConstantsClassVisitor // Note: cv must not override `visitMethod` (it will not be called with the current implementation below)
/** [ClassVisitor] which skips visiting methods where `[shouldSkipMethod] == true`. */
private class SkipMethodClassVisitor(
cv: ClassVisitor,
private val shouldSkipMethod: (JvmMemberSignature.Method) -> Boolean,
) : ClassVisitor(Opcodes.API_VERSION, cv) {
private val result = mutableMapOf<JvmMemberSignature.Method, Long>()
private var classVersion: Int? = null
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
return if (shouldSkipMethod(JvmMemberSignature.Method(name, desc))) {
null
} else {
cv.visitMethod(access, name, desc, signature, exceptions)
}
}
}
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
super.visit(version, access, name, signature, superName, interfaces)
classVersion = version
/** Computes the snapshot of a Java class represented by a [ClassNode]. */
object ClassNodeSnapshotter {
fun snapshotClass(classNode: ClassNode): Long {
val classWriter = ClassWriter(0)
classNode.accept(classWriter)
return classWriter.toByteArray().hashToLong()
}
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
// Note: Do not filter out private methods here because a *public* inline function may actually have a *private* corresponding JVM
// method in the bytecode (see `InlineOnlyKt.isInlineOnlyPrivateInBytecode`).
// Just filter the methods based on the given `inlineFunctionsAndAccessors` set.
val method = JvmMemberSignature.Method(name, desc)
if (method !in inlineFunctionsAndAccessors) return null
val classWriter = ClassWriter(0)
// The `version` and `name` parameters are important (see KT-38857), the others can be null.
classWriter.visit(/* version */ classVersion!!, /* access */ 0, /* name */ "ClassWithOneMethod", null, null, null)
return object : MethodVisitor(Opcodes.API_VERSION, classWriter.visitMethod(access, name, desc, signature, exceptions)) {
override fun visitEnd() {
result[method] = classWriter.toByteArray().md5()
}
fun snapshotClassExcludingMembers(classNode: ClassNode, alsoExcludeKotlinMetaData: Boolean = false): Long {
val originalFields = classNode.fields
val originalMethods = classNode.methods
val originalVisibleAnnotations = classNode.visibleAnnotations
classNode.fields = emptyList()
classNode.methods = emptyList()
if (alsoExcludeKotlinMetaData) {
classNode.visibleAnnotations = originalVisibleAnnotations?.filterNot { it.desc == "Lkotlin/Metadata;" }
}
return snapshotClass(classNode).also {
classNode.fields = originalFields
classNode.methods = originalMethods
classNode.visibleAnnotations = originalVisibleAnnotations
}
}
fun getResult() = result
fun snapshotField(fieldNode: FieldNode): Long {
val classNode = emptyClass()
classNode.fields.add(fieldNode)
return snapshotClass(classNode)
}
fun snapshotMethod(methodNode: MethodNode, classVersion: Int): Long {
val classNode = emptyClass()
classNode.version = classVersion // Class version is required for method bodies (see KT-38857)
classNode.methods.add(methodNode)
return snapshotClass(classNode)
}
/**
* Sorts fields and methods in the given class.
*
* This is useful when we want to ensure a change in the order of the fields and methods doesn't impact the snapshot (i.e., if their
* order has changed in the `.class` file, it shouldn't require recompilation of the other source files).
*/
fun sortClassMembers(classNode: ClassNode) {
classNode.fields.sortWith(compareBy({ it.name }, { it.desc }))
classNode.methods.sortWith(compareBy({ it.name }, { it.desc }))
}
private fun emptyClass() = ClassNode().also {
// A name is required
it.name = "EmptyClass"
}
}
/**
* [DataExternalizer] for the value of a Kotlin constant.
*
* The constants' values are provided by ASM (see the javadoc of [ConstantsClassVisitor.visitField]), so their types can only be the
* following: Integer, Long, Float, Double, String. (Boolean constants have Integer (0, 1) values in ASM.)
*/
object ConstantValueExternalizer : DataExternalizer<Any> by DelegateDataExternalizer(
listOf(
java.lang.Integer::class.java,
java.lang.Long::class.java,
java.lang.Float::class.java,
java.lang.Double::class.java,
java.lang.String::class.java
),
listOf(IntExternalizer, LongExternalizer, FloatExternalizer, DoubleExternalizer, StringExternalizer)
)
fun ByteArray.hashToLong(): Long {
// Note: The returned type `Long` is 64-bit, but we currently don't have a good 64-bit hash function.
// The method below uses `md5` which is 128-bit and converts it to `Long`.
return md5()
}
@@ -274,8 +274,7 @@ class DelegateDataExternalizer<T>(
override fun read(input: DataInput): T {
val typeIndex = input.readByte().toInt()
@Suppress("UNCHECKED_CAST")
return typesExternalizers[typeIndex].read(input) as T
return typesExternalizers[typeIndex].read(input)
}
}
@@ -34,17 +34,13 @@ class ClassFileWithContentsProvider(
val classFile: ClassFile,
val contentsProvider: () -> ByteArray
) {
fun loadContents(): ClassFileWithContents {
val classContents = contentsProvider.invoke()
val classInfo = BasicClassInfo.compute(classContents)
return ClassFileWithContents(classFile, classContents, classInfo)
}
fun loadContents() = ClassFileWithContents(classFile, contentsProvider.invoke())
}
/** Information about the location of a .class file ([ClassFile]) and its contents. */
class ClassFileWithContents(
@Suppress("unused") val classFile: ClassFile,
val contents: ByteArray,
val classInfo: BasicClassInfo
)
val contents: ByteArray
) {
val classInfo: BasicClassInfo = BasicClassInfo.compute(contents)
}
@@ -246,6 +246,22 @@ object ClasspathChangesComputer {
// classes, and symbols in removed classes.
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
// IncrementalJvmCache currently doesn't use the `KotlinClassInfo.extraInfo.classSnapshotExcludingMembers` info when comparing
// classes, so we need to do it here.
// TODO(KT-58289): Ensure IncrementalJvmCache uses that info when comparing classes.
val currentClassSnapshotsExcludingMembers = currentClassSnapshots
.associate { it.classId to it.classMemberLevelSnapshot!!.extraInfo.classSnapshotExcludingMembers }
.filter { it.value != null }
val previousClassSnapshotsExcludingMembers = previousClassSnapshots
.associate { it.classId to it.classMemberLevelSnapshot!!.extraInfo.classSnapshotExcludingMembers }
.filter { it.value != null }
previousClassSnapshotsExcludingMembers.keys.intersect(currentClassSnapshotsExcludingMembers.keys).forEach {
if (previousClassSnapshotsExcludingMembers[it]!! != currentClassSnapshotsExcludingMembers[it]!!) {
// `areSubclassesAffected = false` as we don't need to compute impacted symbols at this step
changesCollector.collectSignature(fqName = it.asSingleFqName(), areSubclassesAffected = false)
}
}
// Get the changes and clean up
val dirtyData = changesCollector.getChangedSymbols(DoNothingICReporter)
workingDir.deleteRecursively()
@@ -8,7 +8,6 @@ package org.jetbrains.kotlin.incremental.classpathDiff
import com.intellij.util.containers.Interner
import com.intellij.util.io.DataExternalizer
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
import org.jetbrains.kotlin.incremental.ConstantValueExternalizer
import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
@@ -162,8 +161,7 @@ internal object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo>
ListExternalizer(StringExternalizer).save(output, info.classHeaderData.toList())
ListExternalizer(StringExternalizer).save(output, info.classHeaderStrings.toList())
NullableValueExternalizer(StringExternalizer).save(output, info.multifileClassName)
MapExternalizer(StringExternalizer, ConstantValueExternalizer).save(output, info.constantsMap)
MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).save(output, info.inlineFunctionsAndAccessorsMap)
ExtraInfoExternalizer.save(output, info.extraInfo)
}
override fun read(input: DataInput): KotlinClassInfo {
@@ -174,8 +172,24 @@ internal object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo>
classHeaderData = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
classHeaderStrings = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
multifileClassName = NullableValueExternalizer(StringExternalizer).read(input),
constantsMap = MapExternalizer(StringExternalizer, ConstantValueExternalizer).read(input),
inlineFunctionsAndAccessorsMap = MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).read(input)
extraInfo = ExtraInfoExternalizer.read(input)
)
}
}
internal object ExtraInfoExternalizer : DataExternalizer<KotlinClassInfo.ExtraInfo> {
override fun save(output: DataOutput, info: KotlinClassInfo.ExtraInfo) {
NullableValueExternalizer(LongExternalizer).save(output, info.classSnapshotExcludingMembers)
MapExternalizer(StringExternalizer, LongExternalizer).save(output, info.constantSnapshots)
MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).save(output, info.inlineFunctionOrAccessorSnapshots)
}
override fun read(input: DataInput): KotlinClassInfo.ExtraInfo {
return KotlinClassInfo.ExtraInfo(
classSnapshotExcludingMembers = NullableValueExternalizer(LongExternalizer).read(input),
constantSnapshots = MapExternalizer(StringExternalizer, LongExternalizer).read(input),
inlineFunctionOrAccessorSnapshots = MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).read(input)
)
}
}
@@ -200,7 +214,7 @@ private object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapsho
}
}
private object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemberLevelSnapshot> {
internal object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemberLevelSnapshot> {
override fun save(output: DataOutput, snapshot: JavaClassMemberLevelSnapshot) {
JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers)
@@ -9,15 +9,23 @@ import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClass
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClassExcludingMembers
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotField
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotMethod
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.sortClassMembers
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForPackageFacade.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.PackagePartProtoData
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
import org.jetbrains.kotlin.incremental.md5
import org.jetbrains.kotlin.incremental.hashToLong
import org.jetbrains.kotlin.incremental.storage.toByteArray
import org.jetbrains.kotlin.konan.file.use
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind.*
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import java.io.Closeable
import java.io.File
import java.util.zip.ZipEntry
@@ -94,7 +102,7 @@ object ClassSnapshotter {
snapshotKotlinClass(clazz, granularity)
}
else -> metrics.measure(BuildTime.SNAPSHOT_JAVA_CLASSES) {
JavaClassSnapshotter.snapshot(clazz, granularity)
snapshotJavaClass(clazz, granularity)
}
}
}
@@ -120,11 +128,7 @@ object ClassSnapshotter {
}
}
/**
* Computes a [KotlinClassSnapshot] of the given Kotlin class.
*
* (The caller must ensure that the given class is a Kotlin class.)
*/
/** Computes a [KotlinClassSnapshot] of the given Kotlin class. */
private fun snapshotKotlinClass(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): KotlinClassSnapshot {
val kotlinClassInfo =
KotlinClassInfo.createFrom(classFile.classInfo.classId, classFile.classInfo.kotlinClassHeader!!, classFile.contents)
@@ -145,13 +149,60 @@ object ClassSnapshotter {
)
MULTIFILE_CLASS -> MultifileClassKotlinClassSnapshot(
classId, classAbiHash, classMemberLevelSnapshot,
constantNames = kotlinClassInfo.constantsMap.keys
constantNames = kotlinClassInfo.extraInfo.constantSnapshots.keys
)
SYNTHETIC_CLASS -> error("Unexpected class $classId with class kind ${SYNTHETIC_CLASS.name} (synthetic classes should have been removed earlier)")
UNKNOWN -> error("Can't handle class $classId with class kind ${UNKNOWN.name}")
}
}
/** Computes a [JavaClassSnapshot] of the given Java class. */
private fun snapshotJavaClass(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): JavaClassSnapshot {
// For incremental compilation, we only care about the ABI info of a class. There are 2 approaches:
// 1. Collect ABI info directly
// 2. Remove non-ABI info from the full class
// Note that for incremental compilation to be correct, all ABI info must be collected exhaustively (now and in the future when
// there are updates to Java/ASM), whereas it is acceptable if non-ABI info is not removed completely.
// In the following, we will use the second approach as it is safer and easier.
val classNode = ClassNode()
val classReader = ClassReader(classFile.contents)
// Note the `parsingOptions` passed to `classReader`:
// - Pass SKIP_CODE as method bodies are not important
// - Do not pass SKIP_DEBUG as debug info (e.g., method parameter names) may be important
classReader.accept(classNode, ClassReader.SKIP_CODE)
sortClassMembers(classNode)
// Remove private fields and methods
fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0
classNode.fields.removeIf { it.access.isPrivate() }
classNode.methods.removeIf { it.access.isPrivate() }
// Snapshot the class
val classMemberLevelSnapshot = if (granularity == CLASS_MEMBER_LEVEL) {
JavaClassMemberLevelSnapshot(
classAbiExcludingMembers = JavaElementSnapshot(classNode.name, snapshotClassExcludingMembers(classNode)),
fieldsAbi = classNode.fields.map { JavaElementSnapshot(it.name, snapshotField(it)) },
methodsAbi = classNode.methods.map { JavaElementSnapshot(it.name, snapshotMethod(it, classNode.version)) }
)
} else {
null
}
val classAbiHash = if (granularity == CLASS_MEMBER_LEVEL) {
JavaClassMemberLevelSnapshotExternalizer.toByteArray(classMemberLevelSnapshot!!).hashToLong()
} else {
snapshotClass(classNode)
}
return JavaClassSnapshot(
classId = classFile.classInfo.classId,
classAbiHash = classAbiHash,
classMemberLevelSnapshot = classMemberLevelSnapshot,
supertypes = classFile.classInfo.supertypes
)
}
}
private sealed interface DirectoryOrJarReader : Closeable {
@@ -227,9 +278,3 @@ private class JarReader(jar: File) : DirectoryOrJarReader {
zipFile.close()
}
}
internal fun ByteArray.hashToLong(): Long {
// Note: The returned type `Long` is 64-bit, but we currently don't have a good 64-bit hash function.
// The method below uses `md5` which is 128-bit and converts it to `Long`.
return md5()
}
@@ -1,95 +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.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassWriter
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.FieldNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
/** Computes a [JavaClassSnapshot] of a Java class. */
object JavaClassSnapshotter {
fun snapshot(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): JavaClassSnapshot {
// 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.
// There are 2 approaches:
// 1. Collect ABI info directly. The collected info must be exhaustive (now and in the future when there are updates to Java/ASM).
// 2. Collect all info and remove non-ABI info. The removed info should be exhaustive, but even if it's not, it is still
// acceptable.
// In the following, we will use the second approach as it is safer.
val abiClass = ClassNode()
// First, collect all info.
// 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_DEBUG seems possible but is *not* set just to be safe, so that we don't skip any info that might be important.
// - SKIP_FRAMES or EXPAND_FRAMES is not needed (and not relevant when SKIP_CODE is set).
val classReader = ClassReader(classFile.contents)
classReader.accept(abiClass, ClassReader.SKIP_CODE)
// Then, remove non-ABI info, which includes:
// - Method bodies: Should have already been removed (see SKIP_CODE above)
// - Private fields and methods
fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0
abiClass.fields.removeIf { it.access.isPrivate() }
abiClass.methods.removeIf { it.access.isPrivate() }
// Normalize the class: Sort fields and methods as their order is not important (we still use List instead of Set as we want the
// serialized snapshot to be deterministic).
abiClass.fields.sortWith(compareBy({ it.name }, { it.desc }))
abiClass.methods.sortWith(compareBy({ it.name }, { it.desc }))
// Snapshot the class
val classAbiHash = snapshotClass(abiClass)
val classMemberLevelSnapshot = if (granularity == CLASS_MEMBER_LEVEL) {
val fieldsAbi = abiClass.fields.map { JavaElementSnapshot(it.name, snapshotField(it)) }
val methodsAbi = abiClass.methods.map { JavaElementSnapshot(it.name, snapshotMethod(it)) }
val classAbiExcludingMembers = abiClass.let {
it.fields.clear()
it.methods.clear()
JavaElementSnapshot(it.name, snapshotClass(it))
}
JavaClassMemberLevelSnapshot(classAbiExcludingMembers, fieldsAbi, methodsAbi)
} else null
return JavaClassSnapshot(
classId = classFile.classInfo.classId,
classAbiHash = classAbiHash,
classMemberLevelSnapshot = classMemberLevelSnapshot,
supertypes = classFile.classInfo.supertypes
)
}
private fun snapshotClass(classNode: ClassNode): Long {
val classWriter = ClassWriter(0)
classNode.accept(classWriter)
return classWriter.toByteArray().hashToLong()
}
private fun snapshotField(fieldNode: FieldNode): Long {
val classNode = emptyClass()
classNode.fields.add(fieldNode)
return snapshotClass(classNode)
}
private fun snapshotMethod(methodNode: MethodNode): Long {
val classNode = emptyClass()
classNode.methods.add(methodNode)
return snapshotClass(classNode)
}
private fun emptyClass() = ClassNode().also {
// We need to provide some minimal info to the class:
// - Name is required.
// - Class version is required if method bodies are considered, but we have removed method bodies in this class, so it's optional.
// - Other info is optional.
it.name = "EmptyClass"
}
}
@@ -259,6 +259,9 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
/** Regression test for KT-55021. */
@Test
fun testRenameFileFacade() {
// Check that classpath changes computation doesn't fail.
// Ideally, the returned changes should be empty (renaming a file facade alone shouldn't cause any `LookupSymbol`s to change), but
// it is currently not the case. However, this is just a small efficiency, not a serious bug.
val changes = computeClasspathChanges(File(testDataDir, "KotlinOnly/testRenameFileFacade/src"), tmpDir)
Changes(
lookupSymbols = setOf(
@@ -269,6 +272,20 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
).assertEquals(changes)
}
/** Regression test for KT-58289.*/
@Test
fun testChangedAnnotations() {
val changes = computeClasspathChanges(File(testDataDir, "KotlinOnly/testChangedAnnotations/src"), tmpDir)
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "SomeClassWithChangedAnnotation", scope = "com.example"),
),
fqNames = setOf(
"com.example.SomeClassWithChangedAnnotation",
)
).assertEquals(changes)
}
/** Tests [SupertypesInheritorsImpact]. */
@Test
override fun testImpactComputation_SupertypesInheritors() {
@@ -0,0 +1,20 @@
package com.example
@AnnotationTwo // Changed from @AnnotationOne
class SomeClassWithChangedAnnotation {
val unchangeProperty = 0
fun unchangedFunction() {}
}
class SomeClass {
@AnnotationTwo // Changed from @AnnotationOne
val propertyWithChangedAnnotation = 0
@AnnotationTwo // Changed from @AnnotationOne
fun functionWithChangedAnnotation() {
}
}
annotation class AnnotationOne
annotation class AnnotationTwo
@@ -0,0 +1,20 @@
package com.example
@AnnotationOne // Will change to @AnnotationTwo
class SomeClassWithChangedAnnotation {
val unchangeProperty = 0
fun unchangedFunction() {}
}
class SomeClass {
@AnnotationOne // Will change to @AnnotationTwo
val propertyWithChangedAnnotation = 0
@AnnotationOne // Will change to @AnnotationTwo
fun functionWithChangedAnnotation() {
}
}
annotation class AnnotationOne
annotation class AnnotationTwo
@@ -12,7 +12,7 @@
},
"local": false
},
"classAbiHash": -6515999856905133685,
"classAbiHash": -6381547343043280587,
"classMemberLevelSnapshot": {
"classAbiExcludingMembers": {
"name": "com/example/SimpleClass",
@@ -27,11 +27,11 @@
"methodsAbi": [
{
"name": "\u003cinit\u003e",
"abiHash": 2413123887428571534
"abiHash": 9200648777950343158
},
{
"name": "publicMethod",
"abiHash": 7574889098198027162
"abiHash": -2847179652577921235
}
]
},
@@ -12,7 +12,7 @@
},
"local": false
},
"classAbiHash": -2605231127310346403,
"classAbiHash": 8283317449409255124,
"supertypes": [
{
"internalName": "java/lang/Object"
@@ -12,7 +12,7 @@
},
"local": false
},
"classAbiHash": -2605231127310346403,
"classAbiHash": 8283317449409255124,
"classMemberLevelSnapshot": {
"classId": {
"packageFqName": {
@@ -43,8 +43,11 @@
"privateFunction",
"publicFunction"
],
"constantsMap": {},
"inlineFunctionsAndAccessorsMap": {},
"extraInfo": {
"classSnapshotExcludingMembers": -3712984790086353322,
"constantSnapshots": {},
"inlineFunctionOrAccessorSnapshots": {}
},
"className$delegate": {
"initializer": {},
"_value": {}