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:
@@ -441,7 +441,7 @@ open class IncrementalJvmCache(
|
|||||||
storageFile: File,
|
storageFile: File,
|
||||||
icContext: IncrementalCompilationContext,
|
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 =
|
operator fun contains(className: JvmClassName): Boolean =
|
||||||
className.internalName in storage
|
className.internalName in storage
|
||||||
@@ -451,7 +451,7 @@ open class IncrementalJvmCache(
|
|||||||
val key = kotlinClassInfo.className.internalName
|
val key = kotlinClassInfo.className.internalName
|
||||||
val oldMap = storage[key] ?: emptyMap()
|
val oldMap = storage[key] ?: emptyMap()
|
||||||
|
|
||||||
val newMap = kotlinClassInfo.constantsMap
|
val newMap = kotlinClassInfo.extraInfo.constantSnapshots
|
||||||
if (newMap.isNotEmpty()) {
|
if (newMap.isNotEmpty()) {
|
||||||
storage[key] = newMap
|
storage[key] = newMap
|
||||||
} else {
|
} else {
|
||||||
@@ -489,8 +489,8 @@ open class IncrementalJvmCache(
|
|||||||
storage.remove(className.internalName)
|
storage.remove(className.internalName)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dumpValue(value: Map<String, Any>): String =
|
override fun dumpValue(value: Map<String, Long>): String =
|
||||||
value.dumpMap(Any::toString)
|
value.dumpMap(Long::toString)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class PackagePartMap(
|
private inner class PackagePartMap(
|
||||||
@@ -592,7 +592,7 @@ open class IncrementalJvmCache(
|
|||||||
val key = kotlinClassInfo.className.internalName
|
val key = kotlinClassInfo.className.internalName
|
||||||
val oldMap = storage[key] ?: emptyMap()
|
val oldMap = storage[key] ?: emptyMap()
|
||||||
|
|
||||||
val newMap = kotlinClassInfo.inlineFunctionsAndAccessorsMap
|
val newMap = kotlinClassInfo.extraInfo.inlineFunctionOrAccessorSnapshots
|
||||||
if (newMap.isNotEmpty()) {
|
if (newMap.isNotEmpty()) {
|
||||||
storage[key] = newMap
|
storage[key] = newMap
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.incremental
|
package org.jetbrains.kotlin.incremental
|
||||||
|
|
||||||
import com.intellij.util.io.DataExternalizer
|
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClassExcludingMembers
|
||||||
import org.jetbrains.kotlin.incremental.storage.*
|
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.InlineFunctionOrAccessor
|
||||||
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
|
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
|
||||||
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
|
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.name.Name
|
||||||
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
||||||
import org.jetbrains.org.objectweb.asm.*
|
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`
|
* 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,
|
* `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.
|
* and should not contain private method signatures, or method implementations.
|
||||||
*/
|
*/
|
||||||
class KotlinClassInfo constructor(
|
class KotlinClassInfo(
|
||||||
val classId: ClassId,
|
val classId: ClassId,
|
||||||
val classKind: KotlinClassHeader.Kind,
|
val classKind: KotlinClassHeader.Kind,
|
||||||
val classHeaderData: Array<String>, // Can be empty
|
val classHeaderData: Array<String>, // Can be empty
|
||||||
val classHeaderStrings: 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 multifileClassName: String?, // Not null iff classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART
|
||||||
val constantsMap: Map<String, Any>,
|
val extraInfo: ExtraInfo
|
||||||
val inlineFunctionsAndAccessorsMap: Map<InlineFunctionOrAccessor, Long>
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/** 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 className: JvmClassName by lazy { JvmClassName.byClassId(classId) }
|
||||||
|
|
||||||
val protoMapValue: ProtoMapValue by lazy {
|
val protoMapValue: ProtoMapValue by lazy {
|
||||||
@@ -84,123 +109,148 @@ class KotlinClassInfo constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
|
fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
|
||||||
val (constants, inlineFunctionsAndAccessors) = getConstantsAndInlineFunctionsOrAccessors(classHeader, classContents)
|
|
||||||
|
|
||||||
return KotlinClassInfo(
|
return KotlinClassInfo(
|
||||||
classId,
|
classId,
|
||||||
classHeader.kind,
|
classHeader.kind,
|
||||||
classHeader.data ?: classHeader.incompatibleData ?: emptyArray(),
|
classHeader.data ?: classHeader.incompatibleData ?: emptyArray(),
|
||||||
classHeader.strings ?: emptyArray(),
|
classHeader.strings ?: emptyArray(),
|
||||||
classHeader.multifileClassName,
|
classHeader.multifileClassName,
|
||||||
constants.mapKeys { it.key.name },
|
extraInfo = getExtraInfo(classHeader, classContents)
|
||||||
inlineFunctionsAndAccessors
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun getExtraInfo(classHeader: KotlinClassHeader, classContents: ByteArray): ExtraInfo {
|
||||||
* Parses the class file only once to get both constants and inline functions/property accessors. This is faster than getting them
|
// Get the list of (non-private) inline functions and accessors from Kotlin class metadata, then find and snapshot them in the bytecode.
|
||||||
* separately in two passes.
|
// Note:
|
||||||
*/
|
// - Some of them may not be found in the bytecode. Specifically, internal/private inline functions/accessors may be removed from the
|
||||||
private fun getConstantsAndInlineFunctionsOrAccessors(
|
// bytecode if code shrinker is used. For example, `kotlin-reflect-1.7.20.jar` contains `/kotlin/reflect/jvm/internal/UtilKt.class` in
|
||||||
classHeader: KotlinClassHeader,
|
// which the internal inline function `reflectionCall` appears in the Kotlin class metadata (also in the source file), but not in the
|
||||||
classContents: ByteArray
|
// bytecode. When that happens, we will ignore those methods. It is safe to ignore because the methods are not declared in the bytecode
|
||||||
): Pair<Map<JvmMemberSignature.Field, Any>, Map<InlineFunctionOrAccessor, Long>> {
|
// and therefore can't be referenced.
|
||||||
val constantsClassVisitor = ConstantsClassVisitor()
|
// - Look for private methods as well because a *public* inline function/accessor may have a *private* corresponding method in the
|
||||||
val inlineFunctionsAndAccessors = inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true)
|
// 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()) {
|
val parsingOptions = if (inlineFunctionsAndAccessors.isNotEmpty()) {
|
||||||
// Handle this case differently to improve performance
|
// Do not pass (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info (e.g., line numbers) are important for inline
|
||||||
// parsingOptions = (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info are not important for constants
|
// functions/accessors
|
||||||
ClassReader(classContents).accept(constantsClassVisitor, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG)
|
0
|
||||||
Pair(constantsClassVisitor.getResult(), emptyMap())
|
|
||||||
} else {
|
} else {
|
||||||
val inlineFunctionsAndAccessorsClassVisitor = InlineFunctionsAndAccessorsClassVisitor(
|
// Pass (SKIP_CODE, SKIP_DEBUG) to improve performance as method bodies and debug info are not important when we're not analyzing
|
||||||
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
|
|
||||||
// inline functions/accessors
|
// inline functions/accessors
|
||||||
ClassReader(classContents).accept(inlineFunctionsAndAccessorsClassVisitor, 0)
|
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class ConstantsClassVisitor : ClassVisitor(Opcodes.API_VERSION) {
|
// Load class contents into a `ClassNode`.
|
||||||
private val result = mutableMapOf<JvmMemberSignature.Field, Any>()
|
// 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? {
|
// Snapshot the class excluding its fields and methods and metadata
|
||||||
if (access and Opcodes.ACC_PRIVATE == Opcodes.ACC_PRIVATE) return null
|
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
|
// Snapshot constants
|
||||||
if (value != null && access and staticFinal == staticFinal) {
|
fun FieldNode.isPrivate() = (access and Opcodes.ACC_PRIVATE) != 0
|
||||||
result[JvmMemberSignature.Field(name, desc)] = value
|
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(
|
/** [ClassVisitor] which skips visiting methods where `[shouldSkipMethod] == true`. */
|
||||||
private val inlineFunctionsAndAccessors: Set<JvmMemberSignature.Method>,
|
private class SkipMethodClassVisitor(
|
||||||
cv: ConstantsClassVisitor // Note: cv must not override `visitMethod` (it will not be called with the current implementation below)
|
cv: ClassVisitor,
|
||||||
|
private val shouldSkipMethod: (JvmMemberSignature.Method) -> Boolean,
|
||||||
) : ClassVisitor(Opcodes.API_VERSION, cv) {
|
) : ClassVisitor(Opcodes.API_VERSION, cv) {
|
||||||
|
|
||||||
private val result = mutableMapOf<JvmMemberSignature.Method, Long>()
|
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
|
||||||
private var classVersion: Int? = null
|
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>?) {
|
/** Computes the snapshot of a Java class represented by a [ClassNode]. */
|
||||||
super.visit(version, access, name, signature, superName, interfaces)
|
object ClassNodeSnapshotter {
|
||||||
classVersion = version
|
|
||||||
|
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? {
|
fun snapshotClassExcludingMembers(classNode: ClassNode, alsoExcludeKotlinMetaData: Boolean = false): Long {
|
||||||
// Note: Do not filter out private methods here because a *public* inline function may actually have a *private* corresponding JVM
|
val originalFields = classNode.fields
|
||||||
// method in the bytecode (see `InlineOnlyKt.isInlineOnlyPrivateInBytecode`).
|
val originalMethods = classNode.methods
|
||||||
// Just filter the methods based on the given `inlineFunctionsAndAccessors` set.
|
val originalVisibleAnnotations = classNode.visibleAnnotations
|
||||||
val method = JvmMemberSignature.Method(name, desc)
|
classNode.fields = emptyList()
|
||||||
if (method !in inlineFunctionsAndAccessors) return null
|
classNode.methods = emptyList()
|
||||||
|
if (alsoExcludeKotlinMetaData) {
|
||||||
val classWriter = ClassWriter(0)
|
classNode.visibleAnnotations = originalVisibleAnnotations?.filterNot { it.desc == "Lkotlin/Metadata;" }
|
||||||
|
}
|
||||||
// The `version` and `name` parameters are important (see KT-38857), the others can be null.
|
return snapshotClass(classNode).also {
|
||||||
classWriter.visit(/* version */ classVersion!!, /* access */ 0, /* name */ "ClassWithOneMethod", null, null, null)
|
classNode.fields = originalFields
|
||||||
|
classNode.methods = originalMethods
|
||||||
return object : MethodVisitor(Opcodes.API_VERSION, classWriter.visitMethod(access, name, desc, signature, exceptions)) {
|
classNode.visibleAnnotations = originalVisibleAnnotations
|
||||||
override fun visitEnd() {
|
|
||||||
result[method] = classWriter.toByteArray().md5()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun ByteArray.hashToLong(): Long {
|
||||||
* [DataExternalizer] for the value of a Kotlin constant.
|
// 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`.
|
||||||
* The constants' values are provided by ASM (see the javadoc of [ConstantsClassVisitor.visitField]), so their types can only be the
|
return md5()
|
||||||
* 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)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -274,8 +274,7 @@ class DelegateDataExternalizer<T>(
|
|||||||
|
|
||||||
override fun read(input: DataInput): T {
|
override fun read(input: DataInput): T {
|
||||||
val typeIndex = input.readByte().toInt()
|
val typeIndex = input.readByte().toInt()
|
||||||
@Suppress("UNCHECKED_CAST")
|
return typesExternalizers[typeIndex].read(input)
|
||||||
return typesExternalizers[typeIndex].read(input) as T
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-9
@@ -34,17 +34,13 @@ class ClassFileWithContentsProvider(
|
|||||||
val classFile: ClassFile,
|
val classFile: ClassFile,
|
||||||
val contentsProvider: () -> ByteArray
|
val contentsProvider: () -> ByteArray
|
||||||
) {
|
) {
|
||||||
|
fun loadContents() = ClassFileWithContents(classFile, contentsProvider.invoke())
|
||||||
fun loadContents(): ClassFileWithContents {
|
|
||||||
val classContents = contentsProvider.invoke()
|
|
||||||
val classInfo = BasicClassInfo.compute(classContents)
|
|
||||||
return ClassFileWithContents(classFile, classContents, classInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Information about the location of a .class file ([ClassFile]) and its contents. */
|
/** Information about the location of a .class file ([ClassFile]) and its contents. */
|
||||||
class ClassFileWithContents(
|
class ClassFileWithContents(
|
||||||
@Suppress("unused") val classFile: ClassFile,
|
@Suppress("unused") val classFile: ClassFile,
|
||||||
val contents: ByteArray,
|
val contents: ByteArray
|
||||||
val classInfo: BasicClassInfo
|
) {
|
||||||
)
|
val classInfo: BasicClassInfo = BasicClassInfo.compute(contents)
|
||||||
|
}
|
||||||
|
|||||||
+16
@@ -246,6 +246,22 @@ object ClasspathChangesComputer {
|
|||||||
// classes, and symbols in removed classes.
|
// classes, and symbols in removed classes.
|
||||||
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
|
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
|
// Get the changes and clean up
|
||||||
val dirtyData = changesCollector.getChangedSymbols(DoNothingICReporter)
|
val dirtyData = changesCollector.getChangedSymbols(DoNothingICReporter)
|
||||||
workingDir.deleteRecursively()
|
workingDir.deleteRecursively()
|
||||||
|
|||||||
+20
-6
@@ -8,7 +8,6 @@ package org.jetbrains.kotlin.incremental.classpathDiff
|
|||||||
import com.intellij.util.containers.Interner
|
import com.intellij.util.containers.Interner
|
||||||
import com.intellij.util.io.DataExternalizer
|
import com.intellij.util.io.DataExternalizer
|
||||||
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
|
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.KotlinClassInfo
|
||||||
import org.jetbrains.kotlin.incremental.storage.*
|
import org.jetbrains.kotlin.incremental.storage.*
|
||||||
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
|
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.classHeaderData.toList())
|
||||||
ListExternalizer(StringExternalizer).save(output, info.classHeaderStrings.toList())
|
ListExternalizer(StringExternalizer).save(output, info.classHeaderStrings.toList())
|
||||||
NullableValueExternalizer(StringExternalizer).save(output, info.multifileClassName)
|
NullableValueExternalizer(StringExternalizer).save(output, info.multifileClassName)
|
||||||
MapExternalizer(StringExternalizer, ConstantValueExternalizer).save(output, info.constantsMap)
|
ExtraInfoExternalizer.save(output, info.extraInfo)
|
||||||
MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).save(output, info.inlineFunctionsAndAccessorsMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(input: DataInput): KotlinClassInfo {
|
override fun read(input: DataInput): KotlinClassInfo {
|
||||||
@@ -174,8 +172,24 @@ internal object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo>
|
|||||||
classHeaderData = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
|
classHeaderData = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
|
||||||
classHeaderStrings = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
|
classHeaderStrings = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
|
||||||
multifileClassName = NullableValueExternalizer(StringExternalizer).read(input),
|
multifileClassName = NullableValueExternalizer(StringExternalizer).read(input),
|
||||||
constantsMap = MapExternalizer(StringExternalizer, ConstantValueExternalizer).read(input),
|
extraInfo = ExtraInfoExternalizer.read(input)
|
||||||
inlineFunctionsAndAccessorsMap = MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).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) {
|
override fun save(output: DataOutput, snapshot: JavaClassMemberLevelSnapshot) {
|
||||||
JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers)
|
JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers)
|
||||||
|
|||||||
+59
-14
@@ -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.BuildTime
|
||||||
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
|
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
|
||||||
import org.jetbrains.kotlin.build.report.metrics.measure
|
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.DifferenceCalculatorForPackageFacade.Companion.getNonPrivateMembers
|
||||||
import org.jetbrains.kotlin.incremental.KotlinClassInfo
|
import org.jetbrains.kotlin.incremental.KotlinClassInfo
|
||||||
import org.jetbrains.kotlin.incremental.PackagePartProtoData
|
import org.jetbrains.kotlin.incremental.PackagePartProtoData
|
||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
|
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.incremental.storage.toByteArray
|
||||||
import org.jetbrains.kotlin.konan.file.use
|
import org.jetbrains.kotlin.konan.file.use
|
||||||
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind.*
|
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind.*
|
||||||
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
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.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
@@ -94,7 +102,7 @@ object ClassSnapshotter {
|
|||||||
snapshotKotlinClass(clazz, granularity)
|
snapshotKotlinClass(clazz, granularity)
|
||||||
}
|
}
|
||||||
else -> metrics.measure(BuildTime.SNAPSHOT_JAVA_CLASSES) {
|
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. */
|
||||||
* Computes a [KotlinClassSnapshot] of the given Kotlin class.
|
|
||||||
*
|
|
||||||
* (The caller must ensure that the given class is a Kotlin class.)
|
|
||||||
*/
|
|
||||||
private fun snapshotKotlinClass(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): KotlinClassSnapshot {
|
private fun snapshotKotlinClass(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): KotlinClassSnapshot {
|
||||||
val kotlinClassInfo =
|
val kotlinClassInfo =
|
||||||
KotlinClassInfo.createFrom(classFile.classInfo.classId, classFile.classInfo.kotlinClassHeader!!, classFile.contents)
|
KotlinClassInfo.createFrom(classFile.classInfo.classId, classFile.classInfo.kotlinClassHeader!!, classFile.contents)
|
||||||
@@ -145,13 +149,60 @@ object ClassSnapshotter {
|
|||||||
)
|
)
|
||||||
MULTIFILE_CLASS -> MultifileClassKotlinClassSnapshot(
|
MULTIFILE_CLASS -> MultifileClassKotlinClassSnapshot(
|
||||||
classId, classAbiHash, classMemberLevelSnapshot,
|
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)")
|
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}")
|
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 {
|
private sealed interface DirectoryOrJarReader : Closeable {
|
||||||
@@ -227,9 +278,3 @@ private class JarReader(jar: File) : DirectoryOrJarReader {
|
|||||||
zipFile.close()
|
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()
|
|
||||||
}
|
|
||||||
|
|||||||
-95
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+17
@@ -259,6 +259,9 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
|||||||
/** Regression test for KT-55021. */
|
/** Regression test for KT-55021. */
|
||||||
@Test
|
@Test
|
||||||
fun testRenameFileFacade() {
|
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)
|
val changes = computeClasspathChanges(File(testDataDir, "KotlinOnly/testRenameFileFacade/src"), tmpDir)
|
||||||
Changes(
|
Changes(
|
||||||
lookupSymbols = setOf(
|
lookupSymbols = setOf(
|
||||||
@@ -269,6 +272,20 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
|||||||
).assertEquals(changes)
|
).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]. */
|
/** Tests [SupertypesInheritorsImpact]. */
|
||||||
@Test
|
@Test
|
||||||
override fun testImpactComputation_SupertypesInheritors() {
|
override fun testImpactComputation_SupertypesInheritors() {
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+20
@@ -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
|
||||||
+20
@@ -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
|
||||||
+3
-3
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"local": false
|
"local": false
|
||||||
},
|
},
|
||||||
"classAbiHash": -6515999856905133685,
|
"classAbiHash": -6381547343043280587,
|
||||||
"classMemberLevelSnapshot": {
|
"classMemberLevelSnapshot": {
|
||||||
"classAbiExcludingMembers": {
|
"classAbiExcludingMembers": {
|
||||||
"name": "com/example/SimpleClass",
|
"name": "com/example/SimpleClass",
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"methodsAbi": [
|
"methodsAbi": [
|
||||||
{
|
{
|
||||||
"name": "\u003cinit\u003e",
|
"name": "\u003cinit\u003e",
|
||||||
"abiHash": 2413123887428571534
|
"abiHash": 9200648777950343158
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "publicMethod",
|
"name": "publicMethod",
|
||||||
"abiHash": 7574889098198027162
|
"abiHash": -2847179652577921235
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"local": false
|
"local": false
|
||||||
},
|
},
|
||||||
"classAbiHash": -2605231127310346403,
|
"classAbiHash": 8283317449409255124,
|
||||||
"supertypes": [
|
"supertypes": [
|
||||||
{
|
{
|
||||||
"internalName": "java/lang/Object"
|
"internalName": "java/lang/Object"
|
||||||
|
|||||||
+6
-3
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"local": false
|
"local": false
|
||||||
},
|
},
|
||||||
"classAbiHash": -2605231127310346403,
|
"classAbiHash": 8283317449409255124,
|
||||||
"classMemberLevelSnapshot": {
|
"classMemberLevelSnapshot": {
|
||||||
"classId": {
|
"classId": {
|
||||||
"packageFqName": {
|
"packageFqName": {
|
||||||
@@ -43,8 +43,11 @@
|
|||||||
"privateFunction",
|
"privateFunction",
|
||||||
"publicFunction"
|
"publicFunction"
|
||||||
],
|
],
|
||||||
"constantsMap": {},
|
"extraInfo": {
|
||||||
"inlineFunctionsAndAccessorsMap": {},
|
"classSnapshotExcludingMembers": -3712984790086353322,
|
||||||
|
"constantSnapshots": {},
|
||||||
|
"inlineFunctionOrAccessorSnapshots": {}
|
||||||
|
},
|
||||||
"className$delegate": {
|
"className$delegate": {
|
||||||
"initializer": {},
|
"initializer": {},
|
||||||
"_value": {}
|
"_value": {}
|
||||||
|
|||||||
Reference in New Issue
Block a user