5
5
6
6
package org.jetbrains.kotlin.incremental
7
7
8
+ import com.intellij.util.io.DataExternalizer
8
9
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClassExcludingMembers
9
- import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotField
10
10
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotMethod
11
11
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.sortClassMembers
12
12
import org.jetbrains.kotlin.incremental.KotlinClassInfo.ExtraInfo
13
- import org.jetbrains.kotlin.incremental.storage.ProtoMapValue
13
+ import org.jetbrains.kotlin.incremental.storage.*
14
14
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
15
15
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
16
16
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
@@ -45,19 +45,32 @@ class KotlinClassInfo(
45
45
class ExtraInfo (
46
46
47
47
/* *
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].
49
50
*
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.)
51
55
*
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.
53
57
*/
54
58
val classSnapshotExcludingMembers : Long? ,
55
59
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
+ */
57
65
val constantSnapshots : Map <String , Long >,
58
66
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 >,
61
74
)
62
75
63
76
val className: JvmClassName by lazy { JvmClassName .byClassId(classId) }
@@ -122,19 +135,29 @@ class KotlinClassInfo(
122
135
}
123
136
124
137
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 ()
137
143
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
+ )
138
161
val parsingOptions = if (inlineFunctionsAndAccessors.isNotEmpty()) {
139
162
// Do not pass (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info (e.g., line numbers) are important for inline
140
163
// functions/accessors
@@ -144,54 +167,64 @@ private fun getExtraInfo(classHeader: KotlinClassHeader, classContents: ByteArra
144
167
// inline functions/accessors
145
168
ClassReader .SKIP_CODE or ClassReader .SKIP_DEBUG
146
169
}
170
+ classReader.accept(selectiveClassVisitor, parsingOptions)
147
171
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
154
173
sortClassMembers(classNode)
155
174
156
- // Snapshot the class excluding its fields and methods and metadata
175
+ // 4. Snapshot the class
157
176
val classSnapshotExcludingMembers = if (classHeader.kind == KotlinClassHeader .Kind .CLASS ) {
158
177
// Also exclude Kotlin metadata (see `ExtraInfo.classSnapshotExcludingMembers`'s kdoc)
159
178
snapshotClassExcludingMembers(classNode, alsoExcludeKotlinMetaData = true )
160
179
} else null
161
180
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
+ }
172
185
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
+ }
178
199
179
200
return ExtraInfo (classSnapshotExcludingMembers, constantSnapshots, inlineFunctionOrAccessorSnapshots)
180
201
}
181
202
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 (
184
207
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 ,
186
210
) : ClassVisitor(Opcodes .API_VERSION , cv) {
187
211
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
+
188
218
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())) {
192
220
cv.visitMethod(access, name, desc, signature, exceptions)
193
- }
221
+ } else null
194
222
}
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
+
195
228
}
196
229
197
230
/* * Computes the snapshot of a Java class represented by a [ClassNode]. */
@@ -227,7 +260,7 @@ object ClassNodeSnapshotter {
227
260
228
261
fun snapshotMethod (methodNode : MethodNode , classVersion : Int ): Long {
229
262
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)
231
264
classNode.methods.add(methodNode)
232
265
return snapshotClass(classNode)
233
266
}
@@ -245,10 +278,29 @@ object ClassNodeSnapshotter {
245
278
246
279
private fun emptyClass () = ClassNode ().also {
247
280
// A name is required
248
- it.name = " EmptyClass "
281
+ it.name = " SomeClass "
249
282
}
250
283
}
251
284
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
+
252
304
fun ByteArray.hashToLong (): Long {
253
305
// Note: The returned type `Long` is 64-bit, but we currently don't have a good 64-bit hash function.
254
306
// The method below uses `md5` which is 128-bit and converts it to `Long`.
0 commit comments