From 0b09be73c6ff3575ab39300327155e1a97ba3a64 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Wed, 26 Apr 2023 14:47:57 +0100 Subject: [PATCH] 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 --- .../kotlin/incremental/IncrementalJvmCache.kt | 10 +- .../kotlin/incremental/KotlinClassInfo.kt | 236 +++++++++++------- .../incremental/storage/externalizers.kt | 3 +- .../incremental/classpathDiff/ClassFile.kt | 14 +- .../classpathDiff/ClasspathChangesComputer.kt | 16 ++ .../ClasspathSnapshotSerializer.kt | 26 +- .../classpathDiff/ClasspathSnapshotter.kt | 73 ++++-- .../classpathDiff/JavaClassSnapshotter.kt | 95 ------- .../ClasspathChangesComputerTest.kt | 17 ++ .../com/example/AnnotationOne.class | Bin 0 -> 465 bytes .../com/example/AnnotationTwo.class | Bin 0 -> 465 bytes .../com/example/SomeClass.class | Bin 0 -> 929 bytes .../SomeClassWithChangedAnnotation.class | Bin 0 -> 813 bytes .../com/example/AnnotationOne.class | Bin 0 -> 465 bytes .../com/example/AnnotationTwo.class | Bin 0 -> 465 bytes .../com/example/SomeClass.class | Bin 0 -> 929 bytes .../SomeClassWithChangedAnnotation.class | Bin 0 -> 813 bytes .../com/example/SomeClass.kt | 20 ++ .../com/example/SomeClass.kt | 20 ++ .../com/example/SimpleClass.json | 6 +- .../com/example/SimpleClass.json | 2 +- .../com/example/SimpleClass.json | 9 +- 22 files changed, 316 insertions(+), 231 deletions(-) delete mode 100644 compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/AnnotationOne.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/AnnotationTwo.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/SomeClass.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/SomeClassWithChangedAnnotation.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/AnnotationOne.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/AnnotationTwo.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/SomeClass.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/SomeClassWithChangedAnnotation.class create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/src/current-classpath/com/example/SomeClass.kt create mode 100644 compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/src/previous-classpath/com/example/SomeClass.kt diff --git a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt index f4c38ce703b..598942de613 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt @@ -441,7 +441,7 @@ open class IncrementalJvmCache( storageFile: File, icContext: IncrementalCompilationContext, ) : - BasicStringMap>(storageFile, MapExternalizer(StringExternalizer, ConstantValueExternalizer), icContext) { + BasicStringMap>(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 = - value.dumpMap(Any::toString) + override fun dumpValue(value: Map): 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 { diff --git a/build-common/src/org/jetbrains/kotlin/incremental/KotlinClassInfo.kt b/build-common/src/org/jetbrains/kotlin/incremental/KotlinClassInfo.kt index 00b76643f2c..025c7fa8cca 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/KotlinClassInfo.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/KotlinClassInfo.kt @@ -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, // Can be empty val classHeaderStrings: Array, // Can be empty val multifileClassName: String?, // Not null iff classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART - val constantsMap: Map, - val inlineFunctionsAndAccessorsMap: Map + 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, + + /** Snapshots of the class's inline functions and property accessors (including their implementation). */ + val inlineFunctionOrAccessorSnapshots: Map + ) + 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> { - 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 = inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true) + val inlineFunctionOrAccessorSignatures: Map = + 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() + // 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 = 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 = 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, - 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() - private var classVersion: Int? = null + override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array?): 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?) { - 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?): 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 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() +} diff --git a/build-common/src/org/jetbrains/kotlin/incremental/storage/externalizers.kt b/build-common/src/org/jetbrains/kotlin/incremental/storage/externalizers.kt index 625b2b512c3..5b1070d436c 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/storage/externalizers.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/storage/externalizers.kt @@ -274,8 +274,7 @@ class DelegateDataExternalizer( 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) } } diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClassFile.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClassFile.kt index 63b6d723e41..e1cf5b88d4d 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClassFile.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClassFile.kt @@ -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) +} diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt index 98ced528c9b..0839ea144ff 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt @@ -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() diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt index d7d399cbfa1..51acefd0659 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt @@ -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 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 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 { + + 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 { +internal object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer { override fun save(output: DataOutput, snapshot: JavaClassMemberLevelSnapshot) { JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers) diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt index b63adfdac97..0f30fc5928a 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotter.kt @@ -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() -} diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt deleted file mode 100644 index f28e3356ac2..00000000000 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/JavaClassSnapshotter.kt +++ /dev/null @@ -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" - } -} diff --git a/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest.kt b/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest.kt index 9125e1bafca..4fad567f052 100644 --- a/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest.kt +++ b/compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest.kt @@ -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() { diff --git a/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/AnnotationOne.class b/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/AnnotationOne.class new file mode 100644 index 0000000000000000000000000000000000000000..dfe12344c9aa6bb729692257eafd6bfd66964252 GIT binary patch literal 465 zcmah_Jx>Bb5Ph?V2PZ1{2~ucf=^?SxMvW$yfEZ94s^!?2gS}hhEd)C%f0TtGmi{Q? zoP}6uY-Zog&YPL{-alSm0UV)9s01eR)I>%PT6w2&Y^=1QiCeL9K!oBzj-{t%eDAe- z0~J^Y8N%+oD!(Z|*bcIUy~eLyZDm!Qt~e%S$5M|JVgH}jYoo*9i3v{o_UguOUJy!+ zp|LuQy{59VFRkQX5sfpfi_(x!0|+$a`Xo%V8l(OpOP)=0t|$vv6sx(nXHlL_>!LPHpe{qrS}N@k%Zt=qn1ns8=9VV=JRS>7 zUrbM9T=|M+?C^WSIXjM=LLM%bkVT=3jE`j>D?V0HWQdZFHLQ0*2{y3Vg-EcKVEYrF CzH@B= literal 0 HcmV?d00001 diff --git a/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/AnnotationTwo.class b/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/AnnotationTwo.class new file mode 100644 index 0000000000000000000000000000000000000000..8bb86052a8aca69995b603e05f7600be52fc9dc5 GIT binary patch literal 465 zcmah_Jx>Bb5Pfrq2PZ1{2~uc9=^?SxMvW$yfEqv>s^!?253ylb}ZCL5^DdnUTYQjPfW1eH1>)dGzit^pQi8@9$M(=NS-N?B>SNuI2}ED@a09ISWBfnVtJ9g4U(X*fgc5PiFG9LG&sC#5t^8z@k|5os$9NFXF=g)j;qYO8?M)7DvKQi z<9~4Gf`o*`0YyFXqY$&EYE$uLSu^j=&deLn`umTs-vHdfRfd@$?$+gj==MTc-;BF* zEfh(T17q;^#J;G9BHFFL*x8eT&LP8K-&c{+4;V7nt~U$F!ggUH$6)EUN*HDvf9!aV zVR2XLm;JaW`}*B0rQ2(5YAst2qbSxwt2jz~`V6(+zq6QMKoOI9a7;1W`d`~6ajZb| z&7Yd@M11RQOf%Nr45Gly@|OWijZUmX71f_fEm}egs^Q)J46TzHlQ#wotV8hwW!N&I z)(S&&G_1J1!d>o-hAwvuaL8Nad1W+Q<;(0*_V9yE^r_=HRlb~mlG|ADGK8$V_Ups< z>vTHlj)rB+$$GqMIkrcO=dD#{Gz-Vqce|tMKGx!vWSDBGNIoBQcVvG{?1U64H{w8q zP0?3|9|hfY+3U+dXxUy z=$9aSfmq#af_(PI7dW2?a9kv7j%ii6M7HoNftX9Igo!kAy7)7^k90U*T=>1%%Tu|H zOsZU@eDg4)3M{4eGFhWtCSSrdVV)-6ByJ{3lgCxOF%@<+;&>B|D^$j@2ov?G$r<|k unQdfzob@s5qk=htsQQ@4!Zuif8qRIQP0V{fft{e3f>MHs1ce0CM1KM<6te*U literal 0 HcmV?d00001 diff --git a/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/SomeClassWithChangedAnnotation.class b/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/current-classpath/com/example/SomeClassWithChangedAnnotation.class new file mode 100644 index 0000000000000000000000000000000000000000..0c74ad8d22bd3405a06f8d3aff34efad34728196 GIT binary patch literal 813 zcma)3O>fgc5PfSqj^n1(NeiSYALUDef^b^6fsi1mlB>|7wgjY{Hp!~l#vfI@DdmH7}h#TI$ukrsjj~EsYgnAWsvIo2zBu8SP-euYWJz)4(g9Sy*<6Hp-78pvu2;EIW z`r!;KfB4Tv+JBpnU8&JeR48Nrg;2c96{ngJ9h&@x==hSiB@u+G! zInUOr8%%o@r~l%9U(p`xNmno|G-NEchf!M$_IW#`O1Y7Ac-Z6vsn2IccP|+ZI$~35 z#`5kkRx%PznM$I}hEK6h8?&;}%6fr>OtPlGM{$)-5A@KKb8BDVd?H}r8bz0~rdsq} zCsdq}P@oP_!dzBaCG1ZHKEwM+g^7UHC)IymE=}F<5=Zmc%z1Lr{_P?=b2V@ylfR0a zl()#JOnH$^mRc|aoDXmzz(rIDVmUw+D=n}TmvFg-LJBvU} literal 0 HcmV?d00001 diff --git a/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/AnnotationOne.class b/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/AnnotationOne.class new file mode 100644 index 0000000000000000000000000000000000000000..dfe12344c9aa6bb729692257eafd6bfd66964252 GIT binary patch literal 465 zcmah_Jx>Bb5Ph?V2PZ1{2~ucf=^?SxMvW$yfEZ94s^!?2gS}hhEd)C%f0TtGmi{Q? zoP}6uY-Zog&YPL{-alSm0UV)9s01eR)I>%PT6w2&Y^=1QiCeL9K!oBzj-{t%eDAe- z0~J^Y8N%+oD!(Z|*bcIUy~eLyZDm!Qt~e%S$5M|JVgH}jYoo*9i3v{o_UguOUJy!+ zp|LuQy{59VFRkQX5sfpfi_(x!0|+$a`Xo%V8l(OpOP)=0t|$vv6sx(nXHlL_>!LPHpe{qrS}N@k%Zt=qn1ns8=9VV=JRS>7 zUrbM9T=|M+?C^WSIXjM=LLM%bkVT=3jE`j>D?V0HWQdZFHLQ0*2{y3Vg-EcKVEYrF CzH@B= literal 0 HcmV?d00001 diff --git a/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/AnnotationTwo.class b/compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest/KotlinOnly/testChangedAnnotations/classes/previous-classpath/com/example/AnnotationTwo.class new file mode 100644 index 0000000000000000000000000000000000000000..8bb86052a8aca69995b603e05f7600be52fc9dc5 GIT binary patch literal 465 zcmah_Jx>Bb5Pfrq2PZ1{2~uc9=^?SxMvW$yfEqv>s^!?253ylb}ZCL5^DdnUTYQjPfW1eH1>)dGzit^pQi8@9$M(=NS-N?B>SNuI2}ED@a09ISWBfnVtJ9g4U(X*fgc5PiFG9LG&sC#5t^8z@k|5lJf!NFXF=g)mAfYEeMyX_G9?HjW*|o2uM$ z<9~4Gf`o*`0YyFXqY$&EYE$uLSu^j=&deLn`umTs-vHdhb%wcC)T_!v(d!4Yx)b%} zdLZIB1IFO(ivv*&M7USoYVOOH&LG8LKUAU8j~G%nZZ>jA!**dI!(i!-iW%nXf9&{x zVR=vLmxHJ;2m0M>r90~#YAxH3!!Xi9t0+u*`V5u+zq6R1LmtyvaLh2=`Cr==aiT!; zEuNY0RD3IxG-KuMFl?Dw{xV>p-i>si!s@2fqAj$b8s0ld(K?wiS!2M!x)eWDhSdnQ zR~Z`PQOV^c?s9KDa=BxGL*63KOXJZRudyfTqYpOG=Z@!;c`f@av%Tb{2wC^-H%IR` z=ycj0kBXL)_ITNHY>yVt+iT2d=1#8fZdcQNtVeChFjH5dd@<}b<=~ZQ1{5jQqm~F7 zVxSB^4!Rq%KaeeLKe+I zzXaJ!#HwZ!AQq{GSL-0#I+mdb6U z66HMQn}-=yU?s6v$r|k<`2uDM^CbB;aWhepJgMSMsIcP^C!26wqcV=^k36k<%p$ifgc5PfSqj^n1(NeiSYALUDef^b{7fsi1mlB>|776MXEn`G5&&d7(;N#k9inVE8J}zinh+fW^f?-KzF*C_gw4b`FV zI-%l(gaUPd66UhXD&cS{@EO5JDog~7KB@lma%t**mpF#UX3mp~?r#^_nX82xnfz7U zq`XZ=Wy*_Wvebkf;e3P(5iX)a5X%v&SZRVOT*Boh3JSi$6$MveJ_B