Extract KotlinClassInfo to a separate class

to reduce the size of IncrementalJvmCache and prepare for the next
change.

^KT-54144 In progress

Handle changes to inline functions/property accessors with `@JvmName`s

If we detect a change in an inline function `foo` with @JvmName
`fooJvmName`, we have two options:
   1. Report that function `foo` has changed
   2. Report that method `fooJvmName` has changed

Similarly, if we detect a change in an inline property accessor with
JvmName `getFoo` of property `foo`, we have two options:
   1. Report that property `foo` has changed
   2. Report that property accessor `getFoo` has changed

The compiler is guaranteed to generate `LookupSymbol`s corresponding to
option 1 when referencing inline functions/property accessors, but it is
not guaranteed to generate `LookupSymbol`s corresponding to option 2.
(Currently the compiler seems to support option 2 for *inline*
functions/property accessors, but that may change.)

Therefore, we will choose option 1 as it is cleaner and safer.

^KT-54144 In progress

Ignore inline functions that are not found in the bytecode

^KT-54144 In progress

Add unit test for handling `@JvmName`s

Test: KotlinOnlyClasspathChangesComputerTest
             #testFunctionsAndPropertyAccessorsWithJvmNames
^KT-54144 Fixed

Small cleanup in IncrementalCompilerRunner

 - Add comment for closing caches
 - Rename providedChangedFiles to changedFiles
 - Tiny clean up in `performWorkBeforeCompilation`
 - Count directories to delete in debug logs

^KT-53015 In progress

Small cleanup in IncrementalCompilerRunner

 - Add comment for closing caches
 - Rename providedChangedFiles to changedFiles
 - Tiny clean up in `performWorkBeforeCompilation`
 - Count directories to delete in debug logs

^KT-53015 In progress
This commit is contained in:
Hung Nguyen
2022-10-13 19:32:14 +01:00
committed by nataliya.valtman
parent fce8b877c8
commit ec3da62672
19 changed files with 501 additions and 365 deletions
@@ -16,6 +16,8 @@
package org.jetbrains.kotlin.incremental
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForClass.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForPackageFacade.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
import org.jetbrains.kotlin.name.FqName
@@ -166,7 +168,7 @@ class ChangesCollector {
}
private fun PackagePartProtoData.collectAllFromPackage(isRemoved: Boolean) {
val memberNames = getNonPrivateMemberNames(includeInlineAccessors = true)
val memberNames = getNonPrivateMembers()
if (isRemoved) {
collectRemovedMembers(packageFqName, memberNames)
} else {
@@ -178,14 +180,14 @@ class ChangesCollector {
val classFqName = nameResolver.getClassId(proto.fqName).asSingleFqName()
if (proto.isCompanionObject) {
val memberNames = getNonPrivateMemberNames(includeInlineAccessors = true)
val memberNames = getNonPrivateMembers()
val collectMember = if (isRemoved) this@ChangesCollector::collectRemovedMember else this@ChangesCollector::collectChangedMember
collectMember(classFqName.parent(), classFqName.shortName().asString())
memberNames.forEach { collectMember(classFqName, it) }
} else {
if (!isRemoved && collectAllMembersForNewClass) {
val memberNames = getNonPrivateMemberNames(includeInlineAccessors = true)
val memberNames = getNonPrivateMembers()
memberNames.forEach { this@ChangesCollector.collectChangedMember(classFqName, it) }
}
@@ -23,27 +23,20 @@ import com.intellij.util.io.EnumeratorStringDescriptor
import gnu.trove.THashSet
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.build.GeneratedJvmClass
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForClass.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForPackageFacade.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.inline.inlineAccessors
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
import org.jetbrains.kotlin.inline.InlineFunction
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
import org.jetbrains.kotlin.inline.InlinePropertyAccessor
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache
import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMemberSignature
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.metadata.jvm.deserialization.ModuleMapping
import org.jetbrains.kotlin.metadata.jvm.serialization.JvmStringTable
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
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.ClassReader.SKIP_CODE
import org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG
import java.io.File
import java.security.MessageDigest
@@ -439,7 +432,7 @@ open class IncrementalJvmCache(
// todo: reuse code with InlineFunctionsMap?
private inner class ConstantsMap(storageFile: File) :
BasicStringMap<LinkedHashMap<String, Any>>(storageFile, LinkedHashMapExternalizer(StringExternalizer, ConstantExternalizer)) {
BasicStringMap<Map<String, Any>>(storageFile, MapExternalizer(StringExternalizer, ConstantValueExternalizer)) {
operator fun contains(className: JvmClassName): Boolean =
className.internalName in storage
@@ -487,7 +480,7 @@ open class IncrementalJvmCache(
storage.remove(className.internalName)
}
override fun dumpValue(value: LinkedHashMap<String, Any>): String =
override fun dumpValue(value: Map<String, Any>): String =
value.dumpMap(Any::toString)
}
@@ -567,7 +560,10 @@ open class IncrementalJvmCache(
}
private inner class InlineFunctionsMap(storageFile: File) :
BasicStringMap<LinkedHashMap<String, Long>>(storageFile, LinkedHashMapExternalizer(StringExternalizer, LongExternalizer)) {
BasicStringMap<Map<InlineFunctionOrAccessor, Long>>(
storageFile,
MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer)
) {
@Synchronized
fun process(kotlinClassInfo: KotlinClassInfo, changesCollector: ChangesCollector) {
@@ -581,34 +577,36 @@ open class IncrementalJvmCache(
storage.remove(key)
}
for (methodSignature in oldMap.keys + newMap.keys) {
val inlineFunctionOrAccessorName = functionNameBySignature(methodSignature)
// Note: If we detect a change in an inline property accessor (e.g., `getFoo`), we have two options:
// 1. Report that property `foo` has changed (and ignore `getFoo`)
// 2. Report that property accessor `getFoo` has changed (and ignore `foo`)
// Both options are possible (the compiler generates `LookupSymbol`s for both `foo` and `getFoo` when `getFoo` is inlined).
// Currently, we choose option 2 as it is simpler (i.e., even though inline property accessors are not immediate members of
// a class/package, we treat them like immediate members).
changesCollector.collectMemberIfValueWasChanged(
kotlinClassInfo.scopeFqName(),
inlineFunctionOrAccessorName,
oldMap[methodSignature],
newMap[methodSignature]
)
// Note: If we detect a change in an inline function `foo` with @JvmName `fooJvmName`, we have two options:
// 1. Report that function `foo` has changed
// 2. Report that method `fooJvmName` has changed
//
// Similarly, if we detect a change in an inline property accessor with JvmName `getFoo` of property `foo`, we have two options:
// 1. Report that property `foo` has changed
// 2. Report that property accessor `getFoo` has changed
//
// The compiler is guaranteed to generate `LookupSymbol`s corresponding to option 1 when referencing inline functions/property
// accessors, but it is not guaranteed to generate `LookupSymbol`s corresponding to option 2. (Currently the compiler seems to
// support option 2 for *inline* functions/property accessors, but that may change.)
//
// In the following, we will choose option 1 as it is cleaner and safer.
val scope = kotlinClassInfo.scopeFqName()
(oldMap.keys + newMap.keys).forEach {
val name = when (it) {
is InlineFunction -> it.kotlinFunctionName
is InlinePropertyAccessor -> it.propertyName
}
changesCollector.collectMemberIfValueWasChanged(scope, name, oldMap[it], newMap[it])
}
}
// TODO get name in better way instead of using substringBefore
private fun functionNameBySignature(signature: String): String =
signature.substringBefore("(")
@Synchronized
fun remove(className: JvmClassName) {
storage.remove(className.internalName)
}
override fun dumpValue(value: LinkedHashMap<String, Long>): String =
value.dumpMap { java.lang.Long.toHexString(it) }
override fun dumpValue(value: Map<InlineFunctionOrAccessor, Long>): String =
value.mapKeys { it.key.jvmMethodSignature.asString() }.dumpMap { java.lang.Long.toHexString(it) }
}
private fun KotlinClassInfo.scopeFqName() = when (classKind) {
@@ -669,182 +667,3 @@ fun <K : Comparable<K>, V> Map<K, V>.dumpMap(dumpValue: (V) -> String): String =
@TestOnly
fun <T : Comparable<T>> Collection<T>.dumpCollection(): String =
"[${sorted().joinToString(", ", transform = Any::toString)}]"
/**
* @param includeInlineAccessors If `true`, also include inline property accessors in the returned list. This is necessary because property
* accessors are not immediate members of a class/package, and in some cases we treat them like immediate members (see
* [org.jetbrains.kotlin.incremental.IncrementalJvmCache.InlineFunctionsMap.process]). Note that here we deal with *inline* property
* accessors only as [org.jetbrains.kotlin.incremental.IncrementalJvmCache.InlineFunctionsMap.process] is written only to handle *inline*
* property accessors (and functions).
*/
fun ProtoData.getNonPrivateMemberNames(includeInlineAccessors: Boolean): List<String> {
return when (this) {
is ClassProtoData -> {
getNonPrivateMembers() + (if (includeInlineAccessors) {
inlineAccessors(proto.propertyList, nameResolver, excludePrivateAccessors = true).map { it.name }
} else emptyList())
}
is PackagePartProtoData -> {
getNonPrivateMembers() + (if (includeInlineAccessors) {
inlineAccessors(proto.propertyList, nameResolver, excludePrivateAccessors = true).map { it.name }
} else emptyList())
}
}
}
/**
* Minimal information about a Kotlin class to compute recompilation-triggering changes during an incremental run of the `KotlinCompile`
* task (see [IncrementalJvmCache.saveClassToCache]).
*
* It's important that this class contain only the minimal required information, as it will be part of the classpath snapshot of the
* `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(
val classId: ClassId,
val classKind: KotlinClassHeader.Kind,
val classHeaderData: Array<String>, // Can be empty
val classHeaderStrings: Array<String>, // Can be empty
val multifileClassName: String?, // Not null iff classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART
val constantsMap: LinkedHashMap<String, Any>,
val inlineFunctionsAndAccessorsMap: LinkedHashMap<String, Long>
) {
val className: JvmClassName by lazy { JvmClassName.byClassId(classId) }
val protoMapValue: ProtoMapValue by lazy {
ProtoMapValue(
isPackageFacade = classKind != KotlinClassHeader.Kind.CLASS,
BitEncoding.decodeBytes(classHeaderData),
classHeaderStrings
)
}
/**
* The [ProtoData] of this class.
*
* NOTE: The caller needs to ensure `classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS` first, as the compiler doesn't write proto
* data to [KotlinClassHeader.Kind.MULTIFILE_CLASS] classes.
*/
val protoData: ProtoData by lazy {
check(classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS) {
"Proto data is not available for KotlinClassHeader.Kind.MULTIFILE_CLASS: $classId"
}
protoMapValue.toProtoData(classId.packageFqName)
}
/** Name of the companion object of this class (default is "Companion") iff this class HAS a companion object, or null otherwise. */
val companionObject: ClassId? by lazy {
if (classKind == KotlinClassHeader.Kind.CLASS) {
(protoData as ClassProtoData).getCompanionObjectName()?.let {
classId.createNestedClassId(Name.identifier(it))
}
} else null
}
/** List of constants defined in this class iff this class IS a companion object, or null otherwise. The list could be empty. */
val constantsInCompanionObject: List<String>? by lazy {
if (classKind == KotlinClassHeader.Kind.CLASS) {
val classProtoData = protoData as ClassProtoData
if (classProtoData.proto.isCompanionObject) {
classProtoData.getConstants()
} else null
} else null
}
companion object {
fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo {
return createFrom(kotlinClass.classId, kotlinClass.classHeader, kotlinClass.fileContents)
}
fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
val (constants, inlineFunctionsAndAccessors) = getConstantsAndInlineFunctionsOrAccessors(classHeader, classContents)
return KotlinClassInfo(
classId,
classHeader.kind,
classHeader.data ?: emptyArray(),
classHeader.strings ?: emptyArray(),
classHeader.multifileClassName,
constants.mapKeysTo(LinkedHashMap()) { it.key.name },
inlineFunctionsAndAccessors.mapKeysTo(LinkedHashMap()) { it.key.asString() },
)
}
}
}
/**
* Parses the class file only once to get both constants and inline functions/property accessors. This is faster than getting them
* separately in two passes.
*/
private fun getConstantsAndInlineFunctionsOrAccessors(
classHeader: KotlinClassHeader,
classContents: ByteArray
): Pair<Map<JvmMemberSignature.Field, Any>, Map<JvmMemberSignature.Method, Long>> {
val constantsClassVisitor = ConstantsClassVisitor()
val inlineFunctionsAndAccessors = inlineFunctionsAndAccessors(classHeader)
return if (inlineFunctionsAndAccessors.isEmpty()) {
// parsingOptions = (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info are not important for constants
ClassReader(classContents).accept(constantsClassVisitor, SKIP_CODE or SKIP_DEBUG)
Pair(constantsClassVisitor.getResult(), emptyMap())
} else {
val inlineFunctionsAndAccessorsClassVisitor =
InlineFunctionsAndAccessorsClassVisitor(inlineFunctionsAndAccessors.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
ClassReader(classContents).accept(inlineFunctionsAndAccessorsClassVisitor, 0)
Pair(constantsClassVisitor.getResult(), inlineFunctionsAndAccessorsClassVisitor.getResult())
}
}
private class ConstantsClassVisitor : ClassVisitor(Opcodes.API_VERSION) {
private val result = mutableMapOf<JvmMemberSignature.Field, Any>()
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
val staticFinal = Opcodes.ACC_STATIC or Opcodes.ACC_FINAL
if (value != null && access and staticFinal == staticFinal) {
result[JvmMemberSignature.Field(name, desc)] = value
}
return null
}
fun getResult() = result
}
private class InlineFunctionsAndAccessorsClassVisitor(
private val inlineFunctionsAndAccessors: Set<JvmMemberSignature.Method>,
cv: ConstantsClassVisitor // Note: cv must not override `visitMethod` (it will not be called with the current implementation below)
) : ClassVisitor(Opcodes.API_VERSION, cv) {
private val result = mutableMapOf<JvmMemberSignature.Method, Long>()
private var classVersion: Int? = null
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
super.visit(version, access, name, signature, superName, interfaces)
classVersion = version
}
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
if (access and Opcodes.ACC_PRIVATE == Opcodes.ACC_PRIVATE) return null
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 getResult() = result
}
@@ -0,0 +1,206 @@
/*
* Copyright 2010-2022 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
import com.intellij.util.io.DataExternalizer
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMemberSignature
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.*
/**
* Minimal information about a Kotlin class to compute recompilation-triggering changes during an incremental run of the `KotlinCompile`
* task (see [IncrementalJvmCache.saveClassToCache]).
*
* It's important that this class contain only the minimal required information, as it will be part of the classpath snapshot of the
* `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(
val classId: ClassId,
val classKind: KotlinClassHeader.Kind,
val classHeaderData: Array<String>, // Can be empty
val classHeaderStrings: Array<String>, // Can be empty
val multifileClassName: String?, // Not null iff classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART
val constantsMap: Map<String, Any>,
val inlineFunctionsAndAccessorsMap: Map<InlineFunctionOrAccessor, Long>
) {
val className: JvmClassName by lazy { JvmClassName.byClassId(classId) }
val protoMapValue: ProtoMapValue by lazy {
ProtoMapValue(
isPackageFacade = classKind != KotlinClassHeader.Kind.CLASS,
BitEncoding.decodeBytes(classHeaderData),
classHeaderStrings
)
}
/**
* The [ProtoData] of this class.
*
* NOTE: The caller needs to ensure `classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS` first, as the compiler doesn't write proto
* data to [KotlinClassHeader.Kind.MULTIFILE_CLASS] classes.
*/
val protoData: ProtoData by lazy {
check(classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS) {
"Proto data is not available for KotlinClassHeader.Kind.MULTIFILE_CLASS: $classId"
}
protoMapValue.toProtoData(classId.packageFqName)
}
/** Name of the companion object of this class (default is "Companion") iff this class HAS a companion object, or null otherwise. */
val companionObject: ClassId? by lazy {
if (classKind == KotlinClassHeader.Kind.CLASS) {
(protoData as ClassProtoData).getCompanionObjectName()?.let {
classId.createNestedClassId(Name.identifier(it))
}
} else null
}
/** List of constants defined in this class iff this class IS a companion object, or null otherwise. The list could be empty. */
val constantsInCompanionObject: List<String>? by lazy {
if (classKind == KotlinClassHeader.Kind.CLASS) {
val classProtoData = protoData as ClassProtoData
if (classProtoData.proto.isCompanionObject) {
classProtoData.getConstants()
} else null
} else null
}
companion object {
fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo {
return createFrom(kotlinClass.classId, kotlinClass.classHeader, kotlinClass.fileContents)
}
fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
val (constants, inlineFunctionsAndAccessors) = getConstantsAndInlineFunctionsOrAccessors(classHeader, classContents)
return KotlinClassInfo(
classId,
classHeader.kind,
classHeader.data ?: emptyArray(),
classHeader.strings ?: emptyArray(),
classHeader.multifileClassName,
constants.mapKeys { it.key.name },
inlineFunctionsAndAccessors
)
}
}
}
/**
* Parses the class file only once to get both constants and inline functions/property accessors. This is faster than getting them
* separately in two passes.
*/
private fun getConstantsAndInlineFunctionsOrAccessors(
classHeader: KotlinClassHeader,
classContents: ByteArray
): Pair<Map<JvmMemberSignature.Field, Any>, Map<InlineFunctionOrAccessor, Long>> {
val constantsClassVisitor = ConstantsClassVisitor()
val inlineFunctionsAndAccessors = inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true)
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())
} 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
// 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)
}
}
private class ConstantsClassVisitor : ClassVisitor(Opcodes.API_VERSION) {
private val result = mutableMapOf<JvmMemberSignature.Field, Any>()
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
val staticFinal = Opcodes.ACC_STATIC or Opcodes.ACC_FINAL
if (value != null && access and staticFinal == staticFinal) {
result[JvmMemberSignature.Field(name, desc)] = value
}
return null
}
fun getResult() = result
}
private class InlineFunctionsAndAccessorsClassVisitor(
private val inlineFunctionsAndAccessors: Set<JvmMemberSignature.Method>,
cv: ConstantsClassVisitor // Note: cv must not override `visitMethod` (it will not be called with the current implementation below)
) : ClassVisitor(Opcodes.API_VERSION, cv) {
private val result = mutableMapOf<JvmMemberSignature.Method, Long>()
private var classVersion: Int? = null
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
super.visit(version, access, name, signature, superName, interfaces)
classVersion = version
}
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
// Note: Do not filter out private methods here because a *public* inline function may actually have a *private* corresponding JVM
// method in the bytecode (see `InlineOnlyKt.isInlineOnlyPrivateInBytecode`).
// Just filter the methods based on the given `inlineFunctionsAndAccessors` set.
val method = JvmMemberSignature.Method(name, desc)
if (method !in inlineFunctionsAndAccessors) return null
val classWriter = ClassWriter(0)
// The `version` and `name` parameters are important (see KT-38857), the others can be null.
classWriter.visit(/* version */ classVersion!!, /* access */ 0, /* name */ "ClassWithOneMethod", null, null, null)
return object : MethodVisitor(Opcodes.API_VERSION, classWriter.visitMethod(access, name, desc, signature, exceptions)) {
override fun visitEnd() {
result[method] = classWriter.toByteArray().md5()
}
}
}
fun getResult() = result
}
/**
* [DataExternalizer] for the value of a Kotlin constant.
*
* The constants' values are provided by ASM (see the javadoc of [ConstantsClassVisitor.visitField]), so their types can only be the
* following: Integer, Long, Float, Double, String. (Boolean constants have Integer (0, 1) values in ASM.)
*/
object ConstantValueExternalizer : DataExternalizer<Any> by DelegateDataExternalizer(
listOf(
java.lang.Integer::class.java,
java.lang.Long::class.java,
java.lang.Float::class.java,
java.lang.Double::class.java,
java.lang.String::class.java
),
listOf(IntExternalizer, LongExternalizer, FloatExternalizer, DoubleExternalizer, StringExternalizer)
)
@@ -22,6 +22,10 @@ import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.IOUtil
import com.intellij.util.io.KeyDescriptor
import org.jetbrains.kotlin.inline.InlineFunction
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
import org.jetbrains.kotlin.inline.InlinePropertyAccessor
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMemberSignature
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
@@ -165,53 +169,6 @@ object StringToLongMapExternalizer : StringMapExternalizer<Long>() {
}
}
/** [DataExternalizer] for a Kotlin constant. */
object ConstantExternalizer : DataExternalizer<Any> {
override fun save(output: DataOutput, value: Any) {
when (value) {
is Int -> {
output.writeByte(Kind.INT.ordinal)
output.writeInt(value)
}
is Float -> {
output.writeByte(Kind.FLOAT.ordinal)
output.writeFloat(value)
}
is Long -> {
output.writeByte(Kind.LONG.ordinal)
output.writeLong(value)
}
is Double -> {
output.writeByte(Kind.DOUBLE.ordinal)
output.writeDouble(value)
}
is String -> {
output.writeByte(Kind.STRING.ordinal)
output.writeString(value)
}
else -> throw IllegalStateException("Unexpected constant class: ${value::class.java}")
}
}
override fun read(input: DataInput): Any {
return when (Kind.values()[input.readByte().toInt()]) {
Kind.INT -> input.readInt()
Kind.FLOAT -> input.readFloat()
Kind.LONG -> input.readLong()
Kind.DOUBLE -> input.readDouble()
Kind.STRING -> input.readString()
}
}
// The constants' values are provided by ASM, so their types can only be the following.
// See https://asm.ow2.io/javadoc/org/objectweb/asm/ClassVisitor.html#visitField(int,java.lang.String,java.lang.String,java.lang.String,java.lang.Object)
// (Note: Boolean constants have Integer (0, 1) values in ASM.)
private enum class Kind {
INT, FLOAT, LONG, DOUBLE, STRING
}
}
fun <T> DataExternalizer<T>.saveToFile(file: File, value: T) {
return DataOutputStream(FileOutputStream(file).buffered()).use {
save(it, value)
@@ -248,6 +205,16 @@ object LongExternalizer : DataExternalizer<Long> {
override fun read(input: DataInput): Long = input.readLong()
}
object FloatExternalizer : DataExternalizer<Float> {
override fun save(output: DataOutput, value: Float) = output.writeFloat(value)
override fun read(input: DataInput): Float = input.readFloat()
}
object DoubleExternalizer : DataExternalizer<Double> {
override fun save(output: DataOutput, value: Double) = output.writeDouble(value)
override fun read(input: DataInput): Double = input.readDouble()
}
object StringExternalizer : DataExternalizer<String> {
override fun save(output: DataOutput, value: String) = IOUtil.writeString(value, output)
override fun read(input: DataInput): String = IOUtil.readString(input)
@@ -268,6 +235,33 @@ object PathStringDescriptor : EnumeratorStringDescriptor() {
}
}
/** [DataExternalizer] that delegates to another [DataExternalizer] depending on the type of the object to externalize. */
class DelegateDataExternalizer<T>(
val types: List<Class<out T>>,
val typesExternalizers: List<DataExternalizer<out T>>
) : DataExternalizer<T> {
init {
check(types.size == typesExternalizers.size)
check(types.size < Byte.MAX_VALUE) // We will writeByte(index), so we need lastIndex (types.size - 1) <= Byte.MAX_VALUE
}
override fun save(output: DataOutput, objectToExternalize: T) {
val type = types.single { it.isAssignableFrom(objectToExternalize!!::class.java) }
val typeIndex = types.indexOf(type)
output.writeByte(typeIndex)
@Suppress("UNCHECKED_CAST")
(typesExternalizers[typeIndex] as DataExternalizer<T>).save(output, objectToExternalize)
}
override fun read(input: DataInput): T {
val typeIndex = input.readByte().toInt()
@Suppress("UNCHECKED_CAST")
return typesExternalizers[typeIndex].read(input) as T
}
}
/**
* [DataExternalizer] for a [Collection].
*
@@ -401,3 +395,53 @@ class LinkedHashMapExternalizer<K, V>(
keyExternalizer: DataExternalizer<K>,
valueExternalizer: DataExternalizer<V>
) : MapExternalizer<K, V, LinkedHashMap<K, V>>(keyExternalizer, valueExternalizer, { size -> LinkedHashMap(size) })
object JvmMethodSignatureExternalizer : DataExternalizer<JvmMemberSignature.Method> {
override fun save(output: DataOutput, method: JvmMemberSignature.Method) {
StringExternalizer.save(output, method.name)
StringExternalizer.save(output, method.desc)
}
override fun read(input: DataInput): JvmMemberSignature.Method {
return JvmMemberSignature.Method(
name = StringExternalizer.read(input),
desc = StringExternalizer.read(input)
)
}
}
object InlineFunctionOrAccessorExternalizer : DataExternalizer<InlineFunctionOrAccessor> by DelegateDataExternalizer(
types = listOf(InlineFunction::class.java, InlinePropertyAccessor::class.java),
typesExternalizers = listOf(InlineFunctionExternalizer, InlinePropertyAccessorExternalizer)
)
private object InlineFunctionExternalizer : DataExternalizer<InlineFunction> {
override fun save(output: DataOutput, function: InlineFunction) {
JvmMethodSignatureExternalizer.save(output, function.jvmMethodSignature)
StringExternalizer.save(output, function.kotlinFunctionName)
}
override fun read(input: DataInput): InlineFunction {
return InlineFunction(
jvmMethodSignature = JvmMethodSignatureExternalizer.read(input),
kotlinFunctionName = StringExternalizer.read(input)
)
}
}
private object InlinePropertyAccessorExternalizer : DataExternalizer<InlinePropertyAccessor> {
override fun save(output: DataOutput, accessor: InlinePropertyAccessor) {
JvmMethodSignatureExternalizer.save(output, accessor.jvmMethodSignature)
StringExternalizer.save(output, accessor.propertyName)
}
override fun read(input: DataInput): InlinePropertyAccessor {
return InlinePropertyAccessor(
jvmMethodSignature = JvmMethodSignatureExternalizer.read(input),
propertyName = StringExternalizer.read(input)
)
}
}