Skip to content

Commit ff612f1

Browse files
hungvietnguyenSpace Team
authored and
Space Team
committed
IC: Small cleanup in KotlinClassInfo.kt
This is to address the not-yet-resolved comments in #5127, plus a few other small (non-functional) changes. ^KT-58986: In progress
1 parent cba7154 commit ff612f1

File tree

8 files changed

+149
-78
lines changed

8 files changed

+149
-78
lines changed

build-common/src/org/jetbrains/kotlin/incremental/KotlinClassInfo.kt

Lines changed: 103 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
package org.jetbrains.kotlin.incremental
77

8+
import com.intellij.util.io.DataExternalizer
89
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClassExcludingMembers
9-
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotField
1010
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotMethod
1111
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.sortClassMembers
1212
import org.jetbrains.kotlin.incremental.KotlinClassInfo.ExtraInfo
13-
import org.jetbrains.kotlin.incremental.storage.ProtoMapValue
13+
import org.jetbrains.kotlin.incremental.storage.*
1414
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
1515
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
1616
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
@@ -45,19 +45,32 @@ class KotlinClassInfo(
4545
class ExtraInfo(
4646

4747
/**
48-
* Snapshot of the class excluding its fields and methods and Kotlin metadata (iff classKind == [KotlinClassHeader.Kind.CLASS]).
48+
* Snapshot of the class excluding its fields and methods and Kotlin metadata. It is not null iff
49+
* [classKind] == [KotlinClassHeader.Kind.CLASS].
4950
*
50-
* For example, the class's annotations which are currently not captured by Kotlin metadata (KT-57919) will be captured here.
51+
* Note: Kotlin metadata is excluded because [ExtraInfo] is meant to contain information that supplements Kotlin metadata. (We have
52+
* a separate logic for comparing protos constructed from Kotlin metadata. That logic considers only changes in protos/Kotlin
53+
* metadata that are important for incremental compilation. If we don't exclude Kotlin metadata here, we might report a change in
54+
* Kotlin metadata even when the change is not important for incremental compilation.)
5155
*
52-
* Note: It also excludes Kotlin metadata as [ExtraInfo] should only contain info not yet captured in Kotlin metadata.
56+
* TODO(KT-59292): Consider removing this info once class annotations are included in Kotlin metadata.
5357
*/
5458
val classSnapshotExcludingMembers: Long?,
5559

56-
/** Snapshots of the class's constants (including their values). The map's keys are the constants' names. */
60+
/**
61+
* Snapshots of the class's non-private constants.
62+
*
63+
* Each entry maps a constant's name to the hash of its value.
64+
*/
5765
val constantSnapshots: Map<String, Long>,
5866

59-
/** Snapshots of the class's inline functions and property accessors (including their implementation). */
60-
val inlineFunctionOrAccessorSnapshots: Map<InlineFunctionOrAccessor, Long>
67+
/**
68+
* Snapshots of the class's non-private inline functions and property accessors.
69+
*
70+
* Each entry maps an inline function or property accessor to the hash of its corresponding method in the bytecode (including the
71+
* method's body).
72+
*/
73+
val inlineFunctionOrAccessorSnapshots: Map<InlineFunctionOrAccessor, Long>,
6174
)
6275

6376
val className: JvmClassName by lazy { JvmClassName.byClassId(classId) }
@@ -122,19 +135,29 @@ class KotlinClassInfo(
122135
}
123136

124137
private fun getExtraInfo(classHeader: KotlinClassHeader, classContents: ByteArray): ExtraInfo {
125-
// Get the list of (non-private) inline functions and accessors from Kotlin class metadata, then find and snapshot them in the bytecode.
126-
// Note:
127-
// - Some of them may not be found in the bytecode. Specifically, internal/private inline functions/accessors may be removed from the
128-
// bytecode if code shrinker is used. For example, `kotlin-reflect-1.7.20.jar` contains `/kotlin/reflect/jvm/internal/UtilKt.class` in
129-
// which the internal inline function `reflectionCall` appears in the Kotlin class metadata (also in the source file), but not in the
130-
// bytecode. When that happens, we will ignore those methods. It is safe to ignore because the methods are not declared in the bytecode
131-
// and therefore can't be referenced.
132-
// - Look for private methods as well because a *public* inline function/accessor may have a *private* corresponding method in the
133-
// bytecode (see `InlineOnlyKt.isInlineOnlyPrivateInBytecode`).
134-
val inlineFunctionsAndAccessors: List<InlineFunctionOrAccessor> = inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true)
135-
val inlineFunctionOrAccessorSignatures: Map<JvmMemberSignature.Method, InlineFunctionOrAccessor> =
136-
inlineFunctionsAndAccessors.associateBy { it.jvmMethodSignature }
138+
val inlineFunctionsAndAccessors: Map<JvmMemberSignature.Method, InlineFunctionOrAccessor> =
139+
inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true).associateBy { it.jvmMethodSignature }
140+
141+
// 1. Create a ClassNode that will contain only required info
142+
val classNode = ClassNode()
137143

144+
// 2. Load the class's contents into the ClassNode, keeping only info that is required to compute `ExtraInfo`:
145+
// - Keep only fields that are non-private constants
146+
// - Keep only methods that are non-private inline functions/accessors
147+
// + Do not filter out private methods because a *non-private* inline function/accessor may have a *private* corresponding method
148+
// in the bytecode (see `InlineOnlyKt.isInlineOnlyPrivateInBytecode`)
149+
// + Do not filter out method bodies
150+
val classReader = ClassReader(classContents)
151+
val selectiveClassVisitor = SelectiveClassVisitor(
152+
classNode,
153+
shouldVisitField = { _: JvmMemberSignature.Field, isPrivate: Boolean, isConstant: Boolean ->
154+
!isPrivate && isConstant
155+
},
156+
shouldVisitMethod = { method: JvmMemberSignature.Method, _: Boolean ->
157+
// Do not filter out private methods (see above comment)
158+
method in inlineFunctionsAndAccessors.keys
159+
}
160+
)
138161
val parsingOptions = if (inlineFunctionsAndAccessors.isNotEmpty()) {
139162
// Do not pass (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info (e.g., line numbers) are important for inline
140163
// functions/accessors
@@ -144,54 +167,64 @@ private fun getExtraInfo(classHeader: KotlinClassHeader, classContents: ByteArra
144167
// inline functions/accessors
145168
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG
146169
}
170+
classReader.accept(selectiveClassVisitor, parsingOptions)
147171

148-
// Load class contents into a `ClassNode`.
149-
// Note that we'll only load methods that are inline functions/accessors (including private methods -- see comment at the top of
150-
// `getExtraInfo`) as we don't need to snapshot the other methods when computing `ExtraInfo`.
151-
val classNode = ClassNode()
152-
val classReader = ClassReader(classContents)
153-
classReader.accept(SkipMethodClassVisitor(classNode) { it !in inlineFunctionOrAccessorSignatures.keys }, parsingOptions)
172+
// 3. Sort fields and methods as their order is not important
154173
sortClassMembers(classNode)
155174

156-
// Snapshot the class excluding its fields and methods and metadata
175+
// 4. Snapshot the class
157176
val classSnapshotExcludingMembers = if (classHeader.kind == KotlinClassHeader.Kind.CLASS) {
158177
// Also exclude Kotlin metadata (see `ExtraInfo.classSnapshotExcludingMembers`'s kdoc)
159178
snapshotClassExcludingMembers(classNode, alsoExcludeKotlinMetaData = true)
160179
} else null
161180

162-
// Snapshot constants
163-
fun FieldNode.isPrivate() = (access and Opcodes.ACC_PRIVATE) != 0
164-
fun FieldNode.isStaticFinal() = (access and (Opcodes.ACC_STATIC or Opcodes.ACC_FINAL)) == (Opcodes.ACC_STATIC or Opcodes.ACC_FINAL)
165-
166-
val constantSnapshots: Map<String, Long> = classNode.fields
167-
.filter { !it.isPrivate() && it.isStaticFinal() }
168-
.associate { it.name to snapshotField(it) }
169-
170-
// Snapshot inline functions and accessors
171-
fun MethodNode.signature() = JvmMemberSignature.Method(name = name, desc = desc)
181+
val constantSnapshots: Map<String, Long> = classNode.fields.associate { fieldNode ->
182+
// Note: `fieldNode` is a constant because we kept only fields that are (non-private) constants in `classNode`
183+
fieldNode.name to (fieldNode.value?.let { ConstantValueExternalizer.toByteArray(it).hashToLong() } ?: 0L)
184+
}
172185

173-
val inlineFunctionOrAccessorSnapshots: Map<InlineFunctionOrAccessor, Long> = classNode.methods
174-
.associate { methodNode ->
175-
// `methodNode` must be an inline function/accessor because we loaded only inline functions/accessors into `classNode`
176-
inlineFunctionOrAccessorSignatures[methodNode.signature()]!! to snapshotMethod(methodNode, classNode.version)
177-
}
186+
val inlineFunctionOrAccessorSnapshots: Map<InlineFunctionOrAccessor, Long> = classNode.methods.associate { methodNode ->
187+
// Note:
188+
// - Each of `classNode.methods` (`methodNode`) is an inline function/accessor because we kept only methods that are (non-private)
189+
// inline functions/accessors in `classNode`.
190+
// - Not all inline functions/accessors have a corresponding method in the bytecode (i.e., it's possible that
191+
// `classNode.methods.size < inlineFunctionsAndAccessors.size`). Specifically, internal/private inline functions/accessors may
192+
// be removed from the bytecode if code shrinker is used. For example, `kotlin-reflect-1.7.20.jar` contains
193+
// `/kotlin/reflect/jvm/internal/UtilKt.class` in which the internal inline function `reflectionCall` appears in the Kotlin
194+
// class metadata (also in the source file), but not in the bytecode. However, we can safely ignore those
195+
// inline functions/accessors because they are not declared in the bytecode and therefore can't be referenced.
196+
val methodSignature = JvmMemberSignature.Method(name = methodNode.name, desc = methodNode.desc)
197+
inlineFunctionsAndAccessors[methodSignature]!! to snapshotMethod(methodNode, classNode.version)
198+
}
178199

179200
return ExtraInfo(classSnapshotExcludingMembers, constantSnapshots, inlineFunctionOrAccessorSnapshots)
180201
}
181202

182-
/** [ClassVisitor] which skips visiting methods where `[shouldSkipMethod] == true`. */
183-
private class SkipMethodClassVisitor(
203+
/**
204+
* [ClassVisitor] which visits only members satisfying the given criteria (`[shouldVisitField] == true` or `[shouldVisitMethod] == true`).
205+
*/
206+
class SelectiveClassVisitor(
184207
cv: ClassVisitor,
185-
private val shouldSkipMethod: (JvmMemberSignature.Method) -> Boolean,
208+
private val shouldVisitField: (JvmMemberSignature.Field, isPrivate: Boolean, isConstant: Boolean) -> Boolean,
209+
private val shouldVisitMethod: (JvmMemberSignature.Method, isPrivate: Boolean) -> Boolean,
186210
) : ClassVisitor(Opcodes.API_VERSION, cv) {
187211

212+
override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? {
213+
return if (shouldVisitField(JvmMemberSignature.Field(name, desc), access.isPrivate(), access.isStaticFinal())) {
214+
cv.visitField(access, name, desc, signature, value)
215+
} else null
216+
}
217+
188218
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
189-
return if (shouldSkipMethod(JvmMemberSignature.Method(name, desc))) {
190-
null
191-
} else {
219+
return if (shouldVisitMethod(JvmMemberSignature.Method(name, desc), access.isPrivate())) {
192220
cv.visitMethod(access, name, desc, signature, exceptions)
193-
}
221+
} else null
194222
}
223+
224+
private fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0
225+
226+
private fun Int.isStaticFinal() = (this and (Opcodes.ACC_STATIC or Opcodes.ACC_FINAL)) == (Opcodes.ACC_STATIC or Opcodes.ACC_FINAL)
227+
195228
}
196229

197230
/** Computes the snapshot of a Java class represented by a [ClassNode]. */
@@ -227,7 +260,7 @@ object ClassNodeSnapshotter {
227260

228261
fun snapshotMethod(methodNode: MethodNode, classVersion: Int): Long {
229262
val classNode = emptyClass()
230-
classNode.version = classVersion // Class version is required for method bodies (see KT-38857)
263+
classNode.version = classVersion // Class version is required when working with methods (without it, ASM may fail -- see KT-38857)
231264
classNode.methods.add(methodNode)
232265
return snapshotClass(classNode)
233266
}
@@ -245,10 +278,29 @@ object ClassNodeSnapshotter {
245278

246279
private fun emptyClass() = ClassNode().also {
247280
// A name is required
248-
it.name = "EmptyClass"
281+
it.name = "SomeClass"
249282
}
250283
}
251284

285+
/**
286+
* [DataExternalizer] for the value of a constant.
287+
*
288+
* A constant's value must be not-null and must be one of the following types: Integer, Long, Float, Double, String (see the javadoc of
289+
* [ClassVisitor.visitField]).
290+
*
291+
* Side note: The value of a Boolean constant is represented as an Integer (0, 1) value.
292+
*/
293+
private object ConstantValueExternalizer : DataExternalizer<Any> by DelegateDataExternalizer(
294+
listOf(
295+
java.lang.Integer::class.java,
296+
java.lang.Long::class.java,
297+
java.lang.Float::class.java,
298+
java.lang.Double::class.java,
299+
java.lang.String::class.java
300+
),
301+
listOf(IntExternalizer, LongExternalizer, FloatExternalizer, DoubleExternalizer, StringExternalizer)
302+
)
303+
252304
fun ByteArray.hashToLong(): Long {
253305
// Note: The returned type `Long` is 64-bit, but we currently don't have a good 64-bit hash function.
254306
// The method below uses `md5` which is 128-bit and converts it to `Long`.

compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClassFile.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,7 @@ class ClassFileWithContents(
4242
@Suppress("unused") val classFile: ClassFile,
4343
val contents: ByteArray
4444
) {
45-
val classInfo: BasicClassInfo = BasicClassInfo.compute(contents)
45+
val classInfo: BasicClassInfo by lazy {
46+
BasicClassInfo.compute(contents)
47+
}
4648
}

compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputer.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,17 +248,20 @@ object ClasspathChangesComputer {
248248

249249
// IncrementalJvmCache currently doesn't use the `KotlinClassInfo.extraInfo.classSnapshotExcludingMembers` info when comparing
250250
// classes, so we need to do it here.
251-
// TODO(KT-58289): Ensure IncrementalJvmCache uses that info when comparing classes.
251+
// TODO(KT-59292): Ensure IncrementalJvmCache uses that info when comparing classes, so we can remove this code.
252252
val currentClassSnapshotsExcludingMembers = currentClassSnapshots
253253
.associate { it.classId to it.classMemberLevelSnapshot!!.extraInfo.classSnapshotExcludingMembers }
254254
.filter { it.value != null }
255-
val previousClassSnapshotsExcludingMembers = previousClassSnapshots
256-
.associate { it.classId to it.classMemberLevelSnapshot!!.extraInfo.classSnapshotExcludingMembers }
257-
.filter { it.value != null }
258-
previousClassSnapshotsExcludingMembers.keys.intersect(currentClassSnapshotsExcludingMembers.keys).forEach {
259-
if (previousClassSnapshotsExcludingMembers[it]!! != currentClassSnapshotsExcludingMembers[it]!!) {
255+
previousClassSnapshots.forEach { previousClassSnapshot ->
256+
val classId = previousClassSnapshot.classId
257+
val currentClassSnapshotExcludingMember = currentClassSnapshotsExcludingMembers[classId]
258+
val previousClassSnapshotExcludingMembers =
259+
previousClassSnapshot.classMemberLevelSnapshot!!.extraInfo.classSnapshotExcludingMembers
260+
if (currentClassSnapshotExcludingMember != null && previousClassSnapshotExcludingMembers != null
261+
&& currentClassSnapshotExcludingMember != previousClassSnapshotExcludingMembers
262+
) {
260263
// `areSubclassesAffected = false` as we don't need to compute impacted symbols at this step
261-
changesCollector.collectSignature(fqName = it.asSingleFqName(), areSubclassesAffected = false)
264+
changesCollector.collectSignature(fqName = classId.asSingleFqName(), areSubclassesAffected = false)
262265
}
263266
}
264267

compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotSerializer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ internal object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo>
177177
}
178178
}
179179

180-
internal object ExtraInfoExternalizer : DataExternalizer<KotlinClassInfo.ExtraInfo> {
180+
private object ExtraInfoExternalizer : DataExternalizer<KotlinClassInfo.ExtraInfo> {
181181

182182
override fun save(output: DataOutput, info: KotlinClassInfo.ExtraInfo) {
183183
NullableValueExternalizer(LongExternalizer).save(output, info.classSnapshotExcludingMembers)

0 commit comments

Comments
 (0)